From e6db2b91ebb759ad4af77e30021b2106fd860277 Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Fri, 5 Jun 2026 11:43:35 +0000 Subject: [PATCH 01/16] test: add Jest infrastructure and helpers unit tests Install Jest ^29, wire up test scripts in package.json, create jest.config.js, add coverage/ to .gitignore, bump CI to Node 26 with npm test step, and add 16 passing unit tests for src/helpers.js (unleadingslashit, untrailingslashit, removeEndSlashes). Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/nodejs.yml | 4 +- .gitignore | 1 + jest.config.js | 7 + package-lock.json | 5225 ++++++++++++++++++++++++++++++---- package.json | 6 +- prd.md | 132 + progress.txt | 8 + test/unit/helpers.test.js | 73 + 8 files changed, 4956 insertions(+), 500 deletions(-) create mode 100644 jest.config.js create mode 100644 prd.md create mode 100644 progress.txt create mode 100644 test/unit/helpers.test.js diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index e4603d4b..c6b94eaf 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -10,10 +10,12 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: '12' + node-version: '26' - name: install dependencies run: npm ci env: CI: true - name: run linter run: npm run lint --silent + - name: run tests + run: npm test diff --git a/.gitignore b/.gitignore index 36e274c3..8ab24a85 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ nbproject/ node_modules notes.md wp-local-docker-* +coverage diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..34936640 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + testEnvironment: 'node', + testMatch: [ '**/test/**/*.test.js' ], + collectCoverageFrom: [ 'src/**/*.js' ], + coverageDirectory: 'coverage', + clearMocks: true, +}; diff --git a/package-lock.json b/package-lock.json index 3d7693bb..6d39e34a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "babel-eslint": "^10.0.3", "eslint": "^7.32.0", "husky": "^4.3.8", + "jest": "^29.7.0", "lint-staged": "^10.5.4" }, "engines": { @@ -69,92 +70,207 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/generator": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.7.tgz", - "integrity": "sha512-p+jPjMG+SI8yvIaxGgeW24u7q9+5+TGpZh8/CuB7RhBKd7RCy8FayNEFNNKrNK/eUcY/4ExQqLmyrvBXKsIcwQ==", + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, "engines": { "node": ">=6.9.0" } @@ -245,10 +361,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", - "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, + "dependencies": { + "@babel/types": "^7.29.7" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -256,50 +375,268 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", - "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.7", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.7", - "@babel/types": "^7.22.5", - "debug": "^4.1.0", - "globals": "^11.1.0" + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -310,6 +647,12 @@ "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==" }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "node_modules/@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -365,105 +708,612 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=8" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "engines": { - "node": ">=6.0.0" + "node": ">=6" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">=6.0.0" + "node": ">=8" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "node_modules/@sindresorhus/slugify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-1.1.2.tgz", - "integrity": "sha512-V9nR/W0Xd9TSGXpZ4iFUcFGhuOJtZX82Fzxj1YISlbSgKvIiNa7eLEZrT0vAraPOt++KHauIVNYgGRgjc13dXA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "dependencies": { - "@sindresorhus/transliterate": "^0.1.1", - "escape-string-regexp": "^4.0.0" + "p-try": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@sindresorhus/transliterate": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-0.1.2.tgz", - "integrity": "sha512-5/kmIOY9FF32nicXH+5yLNTX4NJ4atl7jRgqAJuIn/iyDFXBktOKDxCvyGE/EzmF4ngSUvjXxQUQlQiZ5lfw+w==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "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, "dependencies": { - "escape-string-regexp": "^2.0.0", - "lodash.deburr": "^4.1.0" + "p-limit": "^2.2.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/@sindresorhus/transliterate/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, "engines": { "node": ">=8" } }, - "node_modules/@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", - "dev": true - }, - "node_modules/@vscode/sudo-prompt": { + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true + }, + "node_modules/@sindresorhus/slugify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-1.1.2.tgz", + "integrity": "sha512-V9nR/W0Xd9TSGXpZ4iFUcFGhuOJtZX82Fzxj1YISlbSgKvIiNa7eLEZrT0vAraPOt++KHauIVNYgGRgjc13dXA==", + "dependencies": { + "@sindresorhus/transliterate": "^0.1.1", + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-0.1.2.tgz", + "integrity": "sha512-5/kmIOY9FF32nicXH+5yLNTX4NJ4atl7jRgqAJuIn/iyDFXBktOKDxCvyGE/EzmF4ngSUvjXxQUQlQiZ5lfw+w==", + "dependencies": { + "escape-string-regexp": "^2.0.0", + "lodash.deburr": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "dev": true, + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@vscode/sudo-prompt": { "version": "9.3.2", "resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz", "integrity": "sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw==", @@ -590,13 +1440,26 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/arch": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", - "funding": [ - { - "type": "github", + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "funding": [ + { + "type": "github", "url": "https://github.com/sponsors/feross" }, { @@ -701,6 +1564,125 @@ "eslint": ">= 4.12.1" } }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -726,6 +1708,18 @@ } ] }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -795,6 +1789,48 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -818,6 +1854,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "node_modules/buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -847,6 +1889,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -867,6 +1929,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -883,6 +1954,12 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -967,6 +2044,22 @@ "node": ">=0.8" } }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1021,6 +2114,12 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -1056,6 +2155,27 @@ "node": ">=10.0.0" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1122,6 +2242,15 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -1141,6 +2270,24 @@ "node": ">=0.4.0" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/docker-compose": { "version": "0.23.19", "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.23.19.tgz", @@ -1200,6 +2347,24 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.368", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz", + "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1243,9 +2408,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "engines": { "node": ">=6" } @@ -1502,6 +2667,31 @@ "resolved": "https://registry.npmjs.org/executing-npm-path/-/executing-npm-path-1.0.0.tgz", "integrity": "sha512-d/dZlFCLkKm8nwdzpfQ7JBL2BISg4Fu0bVpZ5nacuT3e6DIxYVb+8tx0eQ+jxquvV/8I+VjJ9g6aEAqjukogkw==" }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1555,6 +2745,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -1713,6 +2912,20 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -1725,6 +2938,15 @@ "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1739,6 +2961,15 @@ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "dev": true }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -1794,15 +3025,6 @@ "node": ">= 6" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1895,6 +3117,12 @@ "hostile": "bin/cmd.js" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -2003,226 +3231,1068 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-package": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/import-package/-/import-package-1.0.0.tgz", - "integrity": "sha512-EEDT2ucOWI/9z/h2mLTRkkusM30/pxSoBT6YvomYpEb/UGll6wOvdFagYraCSBHD+dZSKoVFNYCAgC3V7Nvf1Q==", + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, "dependencies": { - "load-from-cwd-or-npm": "^3.0.1" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/import-local/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">=0.8.19" + "node": ">=8" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "node_modules/import-local/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/p-locate": { + "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, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-package": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/import-package/-/import-package-1.0.0.tgz", + "integrity": "sha512-EEDT2ucOWI/9z/h2mLTRkkusM30/pxSoBT6YvomYpEb/UGll6wOvdFagYraCSBHD+dZSKoVFNYCAgC3V7Nvf1Q==", + "dependencies": { + "load-from-cwd-or-npm": "^3.0.1" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/inquirer": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", + "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inspect-with-kind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz", + "integrity": "sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==", + "dependencies": { + "kind-of": "^6.0.2" + } + }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/jest-changed-files/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6", - "wrap-ansi": "^7.0.0" + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" }, "engines": { - "node": ">=12.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/inspect-with-kind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz", - "integrity": "sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==", - "dependencies": { - "kind-of": "^6.0.2" + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/ip-regex": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", - "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "dependencies": { - "has": "^1.0.3" + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "dependencies": { - "is-extglob": "^2.1.1" + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "node_modules/jest-util/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], "engines": { "node": ">=8" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, "engines": { - "node": ">=0.12.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "has-flag": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2247,15 +4317,15 @@ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-parse-even-better-errors": { @@ -2285,6 +4355,18 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -2318,6 +4400,24 @@ "node": ">=0.10.0" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2524,6 +4624,30 @@ "node": ">=10" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/markdown-it": { "version": "12.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", @@ -2757,6 +4881,21 @@ "node": ">= 6.13.0" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2962,6 +5101,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/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==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3039,6 +5187,12 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3051,6 +5205,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/pkg-dir": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", @@ -3086,7 +5249,33 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "engines": { - "node": ">= 0.8.0" + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/process": { @@ -3111,6 +5300,19 @@ "node": ">=0.4.0" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -3133,6 +5335,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, "node_modules/qs": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", @@ -3163,6 +5381,12 @@ "node": ">=0.10.0" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "node_modules/read-yaml": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-yaml/-/read-yaml-1.1.0.tgz", @@ -3293,6 +5517,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3311,6 +5556,15 @@ "npm-cli-dir": "^3.0.0" } }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -3434,6 +5688,12 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3457,6 +5717,25 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", @@ -3527,6 +5806,27 @@ "node": ">=0.10.0" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3563,6 +5863,19 @@ "node": ">=0.6.19" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3601,6 +5914,15 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -3749,6 +6071,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3780,14 +6116,11 @@ "node": ">=0.6.0" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -3846,6 +6179,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -3862,6 +6204,12 @@ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -3870,6 +6218,36 @@ "node": ">= 10.0.0" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/update-check": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", @@ -3907,6 +6285,20 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -3920,6 +6312,15 @@ "extsprintf": "^1.2.0" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -4000,6 +6401,19 @@ "node": ">=0.10.0" } }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/write-yaml": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/write-yaml/-/write-yaml-1.0.0.tgz", @@ -4095,72 +6509,163 @@ "dev": true }, "@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "requires": { - "@babel/highlight": "^7.22.5" + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true + }, + "@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } } }, "@babel/generator": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.7.tgz", - "integrity": "sha512-p+jPjMG+SI8yvIaxGgeW24u7q9+5+TGpZh8/CuB7RhBKd7RCy8FayNEFNNKrNK/eUcY/4ExQqLmyrvBXKsIcwQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "requires": { - "@babel/types": "^7.22.5", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" } }, - "@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", - "dev": true - }, - "@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } } }, - "@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true + }, + "@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" } }, - "@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" } }, + "@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true + }, "@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true }, + "@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "requires": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + } + }, "@babel/highlight": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", @@ -4231,49 +6736,201 @@ } }, "@babel/parser": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", - "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", - "dev": true + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "requires": { + "@babel/types": "^7.29.7" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.29.7" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.29.7" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.29.7" + } }, "@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" } }, "@babel/traverse": { - "version": "7.22.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", - "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.7", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.7", - "@babel/types": "^7.22.5", - "debug": "^4.1.0", - "globals": "^11.1.0" + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" } }, "@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" } }, "@balena/dockerignore": { @@ -4281,6 +6938,12 @@ "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==" }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -4326,15 +6989,322 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "find-up": { + "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" + } + }, + "locate-path": { + "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" + } + }, + "p-limit": { + "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" + } + }, + "p-locate": { + "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" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true + }, + "@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true + } + } + }, + "@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + } + }, + "@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "requires": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + } + }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + } + }, + "@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, "@jridgewell/resolve-uri": { @@ -4343,36 +7313,28 @@ "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "dev": true }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true - }, "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - }, - "dependencies": { - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - } + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true + }, "@sindresorhus/slugify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-1.1.2.tgz", @@ -4398,12 +7360,134 @@ } } }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "requires": { + "@babel/types": "^7.28.2" + } + }, + "@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "dev": true, + "requires": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, "@vscode/sudo-prompt": { "version": "9.3.2", "resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz", @@ -4490,6 +7574,16 @@ "color-convert": "^2.0.1" } }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "arch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", @@ -4565,6 +7659,100 @@ "resolve": "^1.12.0" } }, + "babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "requires": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + } + }, + "babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4576,6 +7764,12 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "baseline-browser-mapping": { + "version": "2.10.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "dev": true + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -4633,6 +7827,28 @@ "fill-range": "^7.0.1" } }, + "browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "requires": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, "buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -4642,6 +7858,12 @@ "ieee754": "^1.1.13" } }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -4659,6 +7881,12 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" }, + "caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -4673,6 +7901,12 @@ "supports-color": "^7.1.0" } }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, "chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -4689,6 +7923,12 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, + "cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -4743,6 +7983,18 @@ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==" }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4788,6 +8040,12 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -4816,6 +8074,21 @@ "nan": "^2.17.0" } }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -4865,6 +8138,12 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, "defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -4878,6 +8157,18 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, "docker-compose": { "version": "0.23.19", "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.23.19.tgz", @@ -4925,6 +8216,18 @@ "safer-buffer": "^2.1.0" } }, + "electron-to-chromium": { + "version": "1.5.368", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz", + "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==", + "dev": true + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4962,9 +8265,9 @@ } }, "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" }, "escape-string-regexp": { "version": "4.0.0", @@ -5154,6 +8457,25 @@ "resolved": "https://registry.npmjs.org/executing-npm-path/-/executing-npm-path-1.0.0.tgz", "integrity": "sha512-d/dZlFCLkKm8nwdzpfQ7JBL2BISg4Fu0bVpZ5nacuT3e6DIxYVb+8tx0eQ+jxquvV/8I+VjJ9g6aEAqjukogkw==" }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -5198,6 +8520,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -5316,6 +8647,13 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -5328,6 +8666,12 @@ "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -5339,6 +8683,12 @@ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "dev": true }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, "get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -5379,12 +8729,6 @@ "is-glob": "^4.0.1" } }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -5441,6 +8785,12 @@ "through": "^2.3.8" } }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -5504,6 +8854,64 @@ "resolve-from": "^4.0.0" } }, + "import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "dependencies": { + "find-up": { + "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" + } + }, + "locate-path": { + "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" + } + }, + "p-limit": { + "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" + } + }, + "p-locate": { + "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" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + } + } + }, "import-package": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/import-package/-/import-package-1.0.0.tgz", @@ -5579,99 +8987,672 @@ "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==" }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true + }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "requires": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "dependencies": { + "semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + } + }, + "jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "dependencies": { + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + } + } + }, + "jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "requires": {} + } + } + }, + "jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "dependencies": { + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, + "jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true + } + } + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + } + }, + "jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true }, - "is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "requires": { - "has": "^1.0.3" + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" } }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==" + "jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "requires": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "dev": true, "requires": { - "is-extglob": "^2.1.1" + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" } }, - "is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==" + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "requires": {} }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", - "dev": true + "jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + } }, - "is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", - "dev": true + "jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "requires": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + } }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true + "jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "requires": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + } + }, + "jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + } }, - "is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "dependencies": { + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true + } + } }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + } }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "requires": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + } }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } }, "js-tokens": { "version": "4.0.0", @@ -5694,9 +9675,9 @@ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true }, "json-parse-even-better-errors": { @@ -5726,6 +9707,12 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -5751,6 +9738,18 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5909,6 +9908,24 @@ "yallist": "^4.0.0" } }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, "markdown-it": { "version": "12.3.2", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", @@ -6102,6 +10119,18 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6258,6 +10287,12 @@ "aggregate-error": "^3.0.0" } }, + "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==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6314,12 +10349,24 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true + }, "pkg-dir": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", @@ -6352,6 +10399,25 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, "process": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/process/-/process-0.10.1.tgz", @@ -6368,6 +10434,16 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -6387,6 +10463,12 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" }, + "pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true + }, "qs": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", @@ -6410,6 +10492,12 @@ } } }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "read-yaml": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-yaml/-/read-yaml-1.1.0.tgz", @@ -6509,6 +10597,23 @@ "supports-preserve-symlinks-flag": "^1.0.0" } }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -6524,6 +10629,12 @@ "npm-cli-dir": "^3.0.0" } }, + "resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true + }, "restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -6617,6 +10728,12 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -6634,6 +10751,22 @@ "is-fullwidth-code-point": "^3.0.0" } }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", @@ -6684,6 +10817,23 @@ "tweetnacl": "~0.14.0" } }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -6705,6 +10855,16 @@ "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", "dev": true }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -6734,6 +10894,12 @@ "ansi-regex": "^5.0.1" } }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -6841,6 +11007,17 @@ "supports-hyperlinks": "^2.0.0" } }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -6869,10 +11046,10 @@ "os-tmpdir": "~1.0.2" } }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, "to-regex-range": { @@ -6920,6 +11097,12 @@ "prelude-ls": "^1.2.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -6930,11 +11113,27 @@ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, + "undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true + }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" }, + "update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "requires": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + } + }, "update-check": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", @@ -6968,6 +11167,17 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + } + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -6978,6 +11188,15 @@ "extsprintf": "^1.2.0" } }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, "wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -7037,6 +11256,16 @@ "mkdirp": "^0.5.1" } }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, "write-yaml": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/write-yaml/-/write-yaml-1.0.0.tgz", diff --git a/package.json b/package.json index 78fe987f..42e0bda1 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,10 @@ "wpdocker": "./index.js", "wpdocker-hosts": "./hosts.js", "lint": "eslint . --ext .js --ignore-pattern node_modules", - "format": "npm run lint --silent -- --fix" + "format": "npm run lint --silent -- --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" }, "bin": { "wpdocker": "index.js", @@ -84,6 +87,7 @@ "babel-eslint": "^10.0.3", "eslint": "^7.32.0", "husky": "^4.3.8", + "jest": "^29.7.0", "lint-staged": "^10.5.4" }, "lint-staged": { diff --git a/prd.md b/prd.md new file mode 100644 index 00000000..86879b83 --- /dev/null +++ b/prd.md @@ -0,0 +1,132 @@ +# Add Unit & Integration Tests to wp-docker (no source changes) + +## Context + +`wp-docker` is a ~3,700-LOC Node.js CLI tool (CommonJS, vanilla JS, Node 20+, no build +step) for managing Dockerized WordPress dev environments. It currently has **zero +tests** — CI only runs ESLint, and the only safety net is the linter. The goal is to +add a comprehensive unit + integration test suite so future features can be added +without fear of regressions, **without changing any production code** (the architecture +is to be preserved exactly as-is). + +This is feasible because the codebase already has clean seams: pure helper/validator +modules, factory functions with injected dependencies (`makeInquirer({ prompt })`, +`makeDockerCompose(spinner)`), and thin wrappers around external tools +(`src/utils/docker-compose.js`, `src/utils/make-docker.js`, `src/utils/yaml.js`). The +side-effecting modules import their dependencies by name, so they can be isolated with +Jest's module mocking — again, no source edits required. + +Decisions confirmed with the user: **Jest**, **comprehensive** scope, **wire into CI**. + +## Key Decisions / Constraints + +- Node version: use node 26 +- **Framework: Jest.** `.eslintrc.json` already declares `"jest": true` (line 6), so test + globals (`describe`, `it`, `expect`, `jest`) are recognized with **no ESLint change**. + Jest works natively with CommonJS — no Babel/transform needed. +- **No production code changes.** Modules that don't export their internal pure functions + (e.g. `marshalDomains`/`marshalWordPress` in `create/inquirer.js`) are tested + _indirectly_ through their public factory by injecting a fake `prompt`. Side-effecting + modules are tested by mocking their imported dependencies. +- **Tests live in a top-level `test/` directory** (mirroring `src/`). This keeps tests + out of the published npm package — `package.json` `files` only lists + `["src","global","scripts","index.js","hosts.js"]`, so `test/` ships nothing. +- **Test files must pass the existing lint.** `npm run lint` runs `eslint .` over the + whole repo (incl. `test/` and `jest.config.js`), and the Husky pre-commit hook runs + `lint-staged` on `*.js`. So every test file is written in the repo's 10up style: + **tabs** for indent, spaces inside parens/brackets/braces, `template-curly-spacing` + (`expect( foo ).toBe( bar )`, `` `http://${ x }` ``). This is the single most important + detail for not breaking the existing workflow. + +## Setup Changes (config only — not source) + +> ✅ **DONE (2026-06-05):** All setup changes implemented. Jest installed (^29.7.0), scripts added, jest.config.js created, .gitignore updated with `coverage/`, CI workflow bumped to Node 26 with `npm test` step. + +1. **`package.json`** ✅ + - Add devDependency: `jest` (^29, supports Node 20). + - Add scripts: + - `"test": "jest"` + - `"test:watch": "jest --watch"` + - `"test:coverage": "jest --coverage"` +2. **`jest.config.js`** ✅ (new, root, written with tabs / lint-clean): + ```js + module.exports = { + testEnvironment: "node", + testMatch: ["**/test/**/*.test.js"], + collectCoverageFrom: ["src/**/*.js"], + coverageDirectory: "coverage", + clearMocks: true, + }; + ``` +3. **`.github/workflows/nodejs.yml`** ✅ — bump `node-version` `'12'` → `'26'` (matches + `engines`; Node 12 would fail `npm ci` on current deps anyway) and add a step after the + linter: `run: npm test`. +4. **`.gitignore`** ✅ — add `coverage/`. + +## Test Inventory + +### `test/unit/` — pure functions, no mocking + +| File | Module under test | Cases | +| --------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ✅ `helpers.test.js` | `src/helpers.js` | `unleadingslashit`, `untrailingslashit`, `removeEndSlashes` — leading/trailing/both/none, multiple slashes (16 tests, 2026-06-05) | +| `prompt-validators.test.js` | `src/prompt-validators.js` | `validateNotEmpty` (empty/whitespace/valid), `validateBool` (y/yes/n/no/other/non-string passthrough), `parseHostname` (strips `http(s)://`, spaces, path), `parseProxyUrl` (adds protocol, trims slashes) | +| `env-utils.test.js` (pure subset) | `src/env-utils.js` | `envSlug` (slugify behavior), `createDefaultProxy` (adds `http://`, `.com` TLD handling) | +| `configure.test.js` (pure subset) | `src/configure.js` | `createProxyConfig` (placeholder `#{TRY_PROXY}`/`#{PROXY_URL}` substitution against a fixture string), `getDefaults`, `getConfigDirectory`/`getGlobalDirectory`/`getConfigFilePath` (path composition under a stubbed `os.homedir`) | +| `create/inquirer.test.js` | `src/commands/create/inquirer.js` | Call `makeInquirer({ prompt })` with a **stub prompt** returning canned answers; assert the marshaled return object — exercises `marshalDomains` (single vs array, dedupe via Set, extraHosts) and `marshalWordPress` (boolean→{}, title/user/pass/email, type, purify) without touching source | + +### `test/integration/` — real module logic with mocked dependencies + +| File | Module under test | Mocks | Focus | +| ------------------------------------ | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `create/make-docker-compose.test.js` | `src/commands/create/make-docker-compose.js` | `../../configure` (`config.get('snapshotsPath')`), `os.platform`, `process.env.USER`/`process.getuid` for the Linux branch | The generated compose object across matrix: PHP version → correct `images[...]`; `wordpress.type` `dev` vs other → nginx conf + wp-cli volume; `elasticsearch` on/off → service+volume; `certs` on/off → `CERT_NAME`; **Linux vs non-Linux** branch (build args, ssh/aws/wpsnapshots volumes); the `settings.dockerCompose` filter hook. Use a **snapshot** for the full object plus targeted assertions. | +| `utils/docker-compose.test.js` | `src/utils/docker-compose.js` | `docker-compose` npm package | Each proxied method (`down/exec/logs/ps/pullAll/restartAll/run/upAll`) forwards args and returns `out`; throws `err` when `exitCode` is truthy (`interpretComposerResults`); `isRunning` → true when `port` resolves, false when it rejects | +| `environment.test.js` | `src/environment.js` | `./configure`, `./database`, `./env-utils`, `./gateway`, `./utils/docker-compose`, `fs-extra`, `inquirer`, `which`, `@vscode/sudo-prompt` | Orchestration & branching: `start` (pull vs no-pull, calls `gateway.startGlobal` then `compose.upAll`), `stop`, `restart` (`isRunning` true→down first), `deleteEnv` (confirm=false short-circuits; confirm=true removes files/certs/db; `manageHosts` true→sudo hosts removal), `startAll/stopAll/restartAll/deleteAll` iterate envs. Assert call order/args, not Docker behavior. | +| `gateway.test.js` | `src/gateway.js` | `./utils/make-docker` (mock docker w/ `getNetwork`/`getVolume`/`createNetwork`/`createVolume`), `./utils/docker-compose`, `./configure`, `fs`, `netcat/client` | `ensureNetworkExists` (creates when `inspect` rejects, skips when it resolves), `removeNetwork`, `ensureCacheExists`/`removeCacheVolume`, `startGlobal` idempotency via the module-level `started` flag, `waitForDB` (fake timers + mock netcat emitting `data`). Use `jest.resetModules()` per test to reset `started`. | +| `database.test.js` | `src/database.js` | `mysql` (mock `createConnection` → `{ query(sql, cb), destroy() }`) | `create`/`deleteDatabase`/`assignPrivs` emit the correct SQL, `destroy()` the connection, resolve on success, reject on query error | +| `certificates.test.js` | `src/certificates.js` | `child_process` (`execSync`), `fs` (`promises.readFile/writeFile`), `mkcert`, `mkcert-prebuilt`, `./configure` (`getSslCertsDirectory`), `./env-utils` | `getCARoot` (returns trimmed CAROOT), `installCA` (true on success, false on throw), `generate` (reads rootCA files, calls `mkcert.createCert` with `allHosts` incl. wildcards, writes `.crt`/`.key`, returns paths) | + +### `test/helpers/` (shared test utilities, lint-clean) + +- `mock-spinner.js` — factory returning a fake ora spinner (`start/succeed/warn` as `jest.fn()`), used across orchestrator tests to assert spinner-vs-console branches. +- Small mock factories for a fake `docker` (dockerode) instance and a fake mysql connection, to avoid duplication. + +### `test/fixtures/` + +- A sample nginx config string containing `#{TRY_PROXY}` / `#{PROXY_URL}` for the + `createProxyConfig` test (or inline it — small enough). + +## Techniques to note for implementation + +- **Module-level state:** `configure.js` caches `let config = null`; `gateway.js` has + `let started`. Use `jest.resetModules()` + re-`require()` in `beforeEach` for those two + suites so state doesn't leak between tests. +- **`clearMocks: true`** in jest config resets mock call history between tests + automatically. +- **Snapshots** are appropriate for `make-docker-compose` (large stable object); pair with + explicit assertions on the branch-specific fields so failures are readable. +- **Fake timers** (`jest.useFakeTimers()`) for `gateway.waitForDB` polling loop. +- Mock `os.platform()` (not the whole `os`) where only the platform branch matters; or + `jest.mock('os', () => ({ ...jest.requireActual('os'), platform: () => 'linux' }))`. + +## Verification + +1. `npm install` — pulls in Jest. +2. `npm test` — all unit + integration suites pass (green). +3. `npm run test:coverage` — produces a coverage report; confirm the targeted modules + (helpers, validators, env-utils, configure, make-docker-compose, docker-compose + wrapper, environment, gateway, database, certificates) are covered. +4. `npm run lint` — still passes with the new `test/` files and `jest.config.js` + present (proves the no-workflow-breakage constraint). +5. Make a trivial throwaway commit to confirm the Husky `pre-commit` (lint-staged) hook + still succeeds with test files staged, then discard it. +6. Confirm CI: the updated workflow runs `npm run lint` **and** `npm test` on push. + +## Out of scope / future (optional follow-ups) + +- The top-level `src/commands/create.js` orchestrator (large; its two highest-value + pieces — `makeInquirer` and `makeDockerCompose` — are already covered). +- Real end-to-end tests that spin up Docker (these would require Docker in CI; the plan + deliberately mocks the Docker/MySQL boundaries instead). +- Bumping `actions/checkout`/`actions/setup-node` action versions (cosmetic; only the + Node version bump is functionally required). diff --git a/progress.txt b/progress.txt new file mode 100644 index 00000000..55ffbd03 --- /dev/null +++ b/progress.txt @@ -0,0 +1,8 @@ +2026-06-05: Test infrastructure setup + helpers unit tests +- Installed jest@^29.7.0 as devDependency +- Added test/test:watch/test:coverage scripts to package.json +- Created jest.config.js (node env, test/**, coverage from src/**) +- Added coverage/ to .gitignore +- Bumped CI workflow node-version 12→26, added npm test step +- Created test/unit/helpers.test.js: 16 passing tests for unleadingslashit, untrailingslashit, removeEndSlashes +- npm test: 16/16 pass; npm run lint: clean diff --git a/test/unit/helpers.test.js b/test/unit/helpers.test.js new file mode 100644 index 00000000..da49a40d --- /dev/null +++ b/test/unit/helpers.test.js @@ -0,0 +1,73 @@ +'use strict'; + +const { unleadingslashit, untrailingslashit, removeEndSlashes } = require( '../../src/helpers' ); + +describe( 'unleadingslashit', () => { + it( 'removes a leading slash', () => { + expect( unleadingslashit( '/foo' ) ).toBe( 'foo' ); + } ); + + it( 'removes multiple leading slashes', () => { + expect( unleadingslashit( '///foo' ) ).toBe( 'foo' ); + } ); + + it( 'leaves a string with no leading slash unchanged', () => { + expect( unleadingslashit( 'foo' ) ).toBe( 'foo' ); + } ); + + it( 'leaves an empty string unchanged', () => { + expect( unleadingslashit( '' ) ).toBe( '' ); + } ); + + it( 'does not remove a trailing slash', () => { + expect( unleadingslashit( 'foo/' ) ).toBe( 'foo/' ); + } ); +} ); + +describe( 'untrailingslashit', () => { + it( 'removes a trailing slash', () => { + expect( untrailingslashit( 'foo/' ) ).toBe( 'foo' ); + } ); + + it( 'removes only one trailing slash', () => { + expect( untrailingslashit( 'foo//' ) ).toBe( 'foo/' ); + } ); + + it( 'leaves a string with no trailing slash unchanged', () => { + expect( untrailingslashit( 'foo' ) ).toBe( 'foo' ); + } ); + + it( 'leaves an empty string unchanged', () => { + expect( untrailingslashit( '' ) ).toBe( '' ); + } ); + + it( 'does not remove a leading slash', () => { + expect( untrailingslashit( '/foo' ) ).toBe( '/foo' ); + } ); +} ); + +describe( 'removeEndSlashes', () => { + it( 'removes both leading and trailing slashes', () => { + expect( removeEndSlashes( '/foo/' ) ).toBe( 'foo' ); + } ); + + it( 'removes multiple leading slashes', () => { + expect( removeEndSlashes( '///foo' ) ).toBe( 'foo' ); + } ); + + it( 'removes a trailing slash but only one', () => { + expect( removeEndSlashes( 'foo//' ) ).toBe( 'foo/' ); + } ); + + it( 'leaves a string with no slashes unchanged', () => { + expect( removeEndSlashes( 'foo' ) ).toBe( 'foo' ); + } ); + + it( 'leaves an empty string unchanged', () => { + expect( removeEndSlashes( '' ) ).toBe( '' ); + } ); + + it( 'handles slashes on both ends with multiple leading slashes', () => { + expect( removeEndSlashes( '///foo/' ) ).toBe( 'foo' ); + } ); +} ); From 68ddf0579526642314d7ee0c5d88a023e43aede6 Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Fri, 5 Jun 2026 11:45:30 +0000 Subject: [PATCH 02/16] test: add prompt-validators unit tests 27 tests covering validateNotEmpty, validateBool, parseHostname, and parseProxyUrl in the 10up lint-clean style; 43/43 passing. Co-Authored-By: Claude Sonnet 4.6 --- prd.md | 2 +- progress.txt | 4 + test/unit/prompt-validators.test.js | 119 ++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 test/unit/prompt-validators.test.js diff --git a/prd.md b/prd.md index 86879b83..f775e18f 100644 --- a/prd.md +++ b/prd.md @@ -70,7 +70,7 @@ Decisions confirmed with the user: **Jest**, **comprehensive** scope, **wire int | File | Module under test | Cases | | --------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ✅ `helpers.test.js` | `src/helpers.js` | `unleadingslashit`, `untrailingslashit`, `removeEndSlashes` — leading/trailing/both/none, multiple slashes (16 tests, 2026-06-05) | -| `prompt-validators.test.js` | `src/prompt-validators.js` | `validateNotEmpty` (empty/whitespace/valid), `validateBool` (y/yes/n/no/other/non-string passthrough), `parseHostname` (strips `http(s)://`, spaces, path), `parseProxyUrl` (adds protocol, trims slashes) | +| ✅ `prompt-validators.test.js` | `src/prompt-validators.js` | `validateNotEmpty` (empty/whitespace/valid), `validateBool` (y/yes/n/no/other/non-string passthrough), `parseHostname` (strips `http(s)://`, spaces, path), `parseProxyUrl` (adds protocol, trims slashes) — 43 tests total across both unit files (2026-06-05) | | `env-utils.test.js` (pure subset) | `src/env-utils.js` | `envSlug` (slugify behavior), `createDefaultProxy` (adds `http://`, `.com` TLD handling) | | `configure.test.js` (pure subset) | `src/configure.js` | `createProxyConfig` (placeholder `#{TRY_PROXY}`/`#{PROXY_URL}` substitution against a fixture string), `getDefaults`, `getConfigDirectory`/`getGlobalDirectory`/`getConfigFilePath` (path composition under a stubbed `os.homedir`) | | `create/inquirer.test.js` | `src/commands/create/inquirer.js` | Call `makeInquirer({ prompt })` with a **stub prompt** returning canned answers; assert the marshaled return object — exercises `marshalDomains` (single vs array, dedupe via Set, extraHosts) and `marshalWordPress` (boolean→{}, title/user/pass/email, type, purify) without touching source | diff --git a/progress.txt b/progress.txt index 55ffbd03..a2d0ca6a 100644 --- a/progress.txt +++ b/progress.txt @@ -6,3 +6,7 @@ - Bumped CI workflow node-version 12→26, added npm test step - Created test/unit/helpers.test.js: 16 passing tests for unleadingslashit, untrailingslashit, removeEndSlashes - npm test: 16/16 pass; npm run lint: clean + +2026-06-05: prompt-validators unit tests +- Created test/unit/prompt-validators.test.js: 27 passing tests for validateNotEmpty, validateBool, parseHostname, parseProxyUrl +- npm test: 43/43 pass; npm run lint: clean diff --git a/test/unit/prompt-validators.test.js b/test/unit/prompt-validators.test.js new file mode 100644 index 00000000..346bbd3b --- /dev/null +++ b/test/unit/prompt-validators.test.js @@ -0,0 +1,119 @@ +'use strict'; + +const { validateNotEmpty, validateBool, parseHostname, parseProxyUrl } = require( '../../src/prompt-validators' ); + +describe( 'validateNotEmpty', () => { + it( 'returns true for a non-empty string', () => { + expect( validateNotEmpty( 'hello' ) ).toBe( true ); + } ); + + it( 'returns an error message for an empty string', () => { + expect( validateNotEmpty( '' ) ).toBe( 'This field is required' ); + } ); + + it( 'returns an error message for a whitespace-only string', () => { + expect( validateNotEmpty( ' ' ) ).toBe( 'This field is required' ); + } ); + + it( 'returns true for a string with leading/trailing whitespace around content', () => { + expect( validateNotEmpty( ' hello ' ) ).toBe( true ); + } ); +} ); + +describe( 'validateBool', () => { + it( 'returns "true" for "y"', () => { + expect( validateBool( 'y' ) ).toBe( 'true' ); + } ); + + it( 'returns "true" for "Y" (case-insensitive)', () => { + expect( validateBool( 'Y' ) ).toBe( 'true' ); + } ); + + it( 'returns "true" for "yes"', () => { + expect( validateBool( 'yes' ) ).toBe( 'true' ); + } ); + + it( 'returns "true" for "YES" (case-insensitive)', () => { + expect( validateBool( 'YES' ) ).toBe( 'true' ); + } ); + + it( 'returns "false" for "n"', () => { + expect( validateBool( 'n' ) ).toBe( 'false' ); + } ); + + it( 'returns "false" for "no"', () => { + expect( validateBool( 'no' ) ).toBe( 'false' ); + } ); + + it( 'returns "false" for "NO" (case-insensitive)', () => { + expect( validateBool( 'NO' ) ).toBe( 'false' ); + } ); + + it( 'returns the original string for an unrecognized value', () => { + expect( validateBool( 'maybe' ) ).toBe( 'maybe' ); + } ); + + it( 'passes through a non-string value unchanged', () => { + expect( validateBool( true ) ).toBe( true ); + } ); + + it( 'passes through a numeric value unchanged', () => { + expect( validateBool( 42 ) ).toBe( 42 ); + } ); +} ); + +describe( 'parseHostname', () => { + it( 'strips an http:// prefix', () => { + expect( parseHostname( 'http://example.com' ) ).toBe( 'example.com' ); + } ); + + it( 'strips an https:// prefix', () => { + expect( parseHostname( 'https://example.com' ) ).toBe( 'example.com' ); + } ); + + it( 'strips an HTTP:// prefix (case-insensitive)', () => { + expect( parseHostname( 'HTTP://example.com' ) ).toBe( 'example.com' ); + } ); + + it( 'removes a path component, keeping only the hostname', () => { + expect( parseHostname( 'example.com/some/path' ) ).toBe( 'example.com' ); + } ); + + it( 'removes both protocol and path', () => { + expect( parseHostname( 'https://example.com/some/path' ) ).toBe( 'example.com' ); + } ); + + it( 'removes spaces from the value', () => { + expect( parseHostname( 'example .com' ) ).toBe( 'example.com' ); + } ); + + it( 'returns a plain hostname unchanged', () => { + expect( parseHostname( 'example.com' ) ).toBe( 'example.com' ); + } ); +} ); + +describe( 'parseProxyUrl', () => { + it( 'adds http:// when no protocol is present and value is longer than 3 chars', () => { + expect( parseProxyUrl( 'example.com' ) ).toBe( 'http://example.com' ); + } ); + + it( 'does not add a protocol when one is already present (http)', () => { + expect( parseProxyUrl( 'http://example.com' ) ).toBe( 'http://example.com' ); + } ); + + it( 'does not add a protocol when one is already present (https)', () => { + expect( parseProxyUrl( 'https://example.com' ) ).toBe( 'https://example.com' ); + } ); + + it( 'does not modify a value with length <= 3', () => { + expect( parseProxyUrl( 'foo' ) ).toBe( 'foo' ); + } ); + + it( 'removes a trailing slash', () => { + expect( parseProxyUrl( 'http://example.com/' ) ).toBe( 'http://example.com' ); + } ); + + it( 'adds protocol and removes trailing slash together', () => { + expect( parseProxyUrl( 'example.com/' ) ).toBe( 'http://example.com' ); + } ); +} ); From 6a30557fab00c5eb7c32ce509a93fbb038143850 Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Fri, 5 Jun 2026 11:47:44 +0000 Subject: [PATCH 03/16] test: add env-utils unit tests for envSlug and createDefaultProxy 13 tests covering slugify behavior and proxy URL construction (http prefix, .com TLD replacement, no-dot case). Co-Authored-By: Claude Sonnet 4.6 --- prd.md | 2 +- progress.txt | 4 +++ test/unit/env-utils.test.js | 59 +++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 test/unit/env-utils.test.js diff --git a/prd.md b/prd.md index f775e18f..53a60aef 100644 --- a/prd.md +++ b/prd.md @@ -71,7 +71,7 @@ Decisions confirmed with the user: **Jest**, **comprehensive** scope, **wire int | --------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ✅ `helpers.test.js` | `src/helpers.js` | `unleadingslashit`, `untrailingslashit`, `removeEndSlashes` — leading/trailing/both/none, multiple slashes (16 tests, 2026-06-05) | | ✅ `prompt-validators.test.js` | `src/prompt-validators.js` | `validateNotEmpty` (empty/whitespace/valid), `validateBool` (y/yes/n/no/other/non-string passthrough), `parseHostname` (strips `http(s)://`, spaces, path), `parseProxyUrl` (adds protocol, trims slashes) — 43 tests total across both unit files (2026-06-05) | -| `env-utils.test.js` (pure subset) | `src/env-utils.js` | `envSlug` (slugify behavior), `createDefaultProxy` (adds `http://`, `.com` TLD handling) | +| ✅ `env-utils.test.js` (pure subset) | `src/env-utils.js` | `envSlug` (slugify behavior), `createDefaultProxy` (adds `http://`, `.com` TLD handling) — 13 tests (2026-06-05) | | `configure.test.js` (pure subset) | `src/configure.js` | `createProxyConfig` (placeholder `#{TRY_PROXY}`/`#{PROXY_URL}` substitution against a fixture string), `getDefaults`, `getConfigDirectory`/`getGlobalDirectory`/`getConfigFilePath` (path composition under a stubbed `os.homedir`) | | `create/inquirer.test.js` | `src/commands/create/inquirer.js` | Call `makeInquirer({ prompt })` with a **stub prompt** returning canned answers; assert the marshaled return object — exercises `marshalDomains` (single vs array, dedupe via Set, extraHosts) and `marshalWordPress` (boolean→{}, title/user/pass/email, type, purify) without touching source | diff --git a/progress.txt b/progress.txt index a2d0ca6a..617ed6f4 100644 --- a/progress.txt +++ b/progress.txt @@ -10,3 +10,7 @@ 2026-06-05: prompt-validators unit tests - Created test/unit/prompt-validators.test.js: 27 passing tests for validateNotEmpty, validateBool, parseHostname, parseProxyUrl - npm test: 43/43 pass; npm run lint: clean + +2026-06-05: env-utils unit tests (pure subset) +- Created test/unit/env-utils.test.js: 13 passing tests for envSlug (slugify behavior) and createDefaultProxy (http prefix, .com TLD handling) +- npm test: 56/56 pass; npm run lint: clean diff --git a/test/unit/env-utils.test.js b/test/unit/env-utils.test.js new file mode 100644 index 00000000..5ca80e84 --- /dev/null +++ b/test/unit/env-utils.test.js @@ -0,0 +1,59 @@ +'use strict'; + +const { envSlug, createDefaultProxy } = require( '../../src/env-utils' ); + +describe( 'envSlug', () => { + it( 'returns a plain lowercase word unchanged', () => { + expect( envSlug( 'mysite' ) ).toBe( 'mysite' ); + } ); + + it( 'converts spaces to hyphens', () => { + expect( envSlug( 'hello world' ) ).toBe( 'hello-world' ); + } ); + + it( 'lowercases uppercase input', () => { + expect( envSlug( 'MySite' ) ).toBe( 'my-site' ); + } ); + + it( 'converts dots to hyphens', () => { + expect( envSlug( 'my.site.com' ) ).toBe( 'my-site-com' ); + } ); + + it( 'converts underscores to hyphens', () => { + expect( envSlug( 'test_env' ) ).toBe( 'test-env' ); + } ); + + it( 'strips leading and trailing spaces', () => { + expect( envSlug( ' leading spaces ' ) ).toBe( 'leading-spaces' ); + } ); + + it( 'preserves already-slugified input', () => { + expect( envSlug( 'hello-world' ) ).toBe( 'hello-world' ); + } ); +} ); + +describe( 'createDefaultProxy', () => { + it( 'prepends http:// to a plain hostname', () => { + expect( createDefaultProxy( 'example.com' ) ).toBe( 'http://example.com' ); + } ); + + it( 'replaces a non-com TLD with com', () => { + expect( createDefaultProxy( 'example.net' ) ).toBe( 'http://example.com' ); + } ); + + it( 'appends .com when there is no dot in the hostname', () => { + expect( createDefaultProxy( 'example' ) ).toBe( 'http://example.com' ); + } ); + + it( 'replaces only the last TLD segment for a subdomain', () => { + expect( createDefaultProxy( 'sub.example.net' ) ).toBe( 'http://sub.example.com' ); + } ); + + it( 'strips a trailing slash from the value', () => { + expect( createDefaultProxy( 'example.com/' ) ).toBe( 'http://example.com' ); + } ); + + it( 'strips leading and trailing slashes from the value', () => { + expect( createDefaultProxy( '/example.com/' ) ).toBe( 'http://example.com' ); + } ); +} ); From a4083d9051cfe5cc0ace2b8fd198b739ed4dc0de Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Fri, 5 Jun 2026 11:50:09 +0000 Subject: [PATCH 04/16] test: add configure unit tests for pure functions Tests getConfigDirectory, getGlobalDirectory, getDefaults (path composition under a mocked os.homedir), and createProxyConfig (placeholder substitution, multi-placeholder, and passthrough cases). 68/68 tests pass. Co-Authored-By: Claude Sonnet 4.6 --- prd.md | 2 +- progress.txt | 5 +++ test/unit/configure.test.js | 81 +++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 test/unit/configure.test.js diff --git a/prd.md b/prd.md index 53a60aef..c19e363e 100644 --- a/prd.md +++ b/prd.md @@ -72,7 +72,7 @@ Decisions confirmed with the user: **Jest**, **comprehensive** scope, **wire int | ✅ `helpers.test.js` | `src/helpers.js` | `unleadingslashit`, `untrailingslashit`, `removeEndSlashes` — leading/trailing/both/none, multiple slashes (16 tests, 2026-06-05) | | ✅ `prompt-validators.test.js` | `src/prompt-validators.js` | `validateNotEmpty` (empty/whitespace/valid), `validateBool` (y/yes/n/no/other/non-string passthrough), `parseHostname` (strips `http(s)://`, spaces, path), `parseProxyUrl` (adds protocol, trims slashes) — 43 tests total across both unit files (2026-06-05) | | ✅ `env-utils.test.js` (pure subset) | `src/env-utils.js` | `envSlug` (slugify behavior), `createDefaultProxy` (adds `http://`, `.com` TLD handling) — 13 tests (2026-06-05) | -| `configure.test.js` (pure subset) | `src/configure.js` | `createProxyConfig` (placeholder `#{TRY_PROXY}`/`#{PROXY_URL}` substitution against a fixture string), `getDefaults`, `getConfigDirectory`/`getGlobalDirectory`/`getConfigFilePath` (path composition under a stubbed `os.homedir`) | +| ✅ `configure.test.js` (pure subset) | `src/configure.js` | `createProxyConfig` (placeholder `#{TRY_PROXY}`/`#{PROXY_URL}` substitution, multi-placeholder, no-placeholder passthrough), `getDefaults` (paths + booleans), `getConfigDirectory`/`getGlobalDirectory` (path composition under a stubbed `os.homedir`) — 12 tests (2026-06-05) | | `create/inquirer.test.js` | `src/commands/create/inquirer.js` | Call `makeInquirer({ prompt })` with a **stub prompt** returning canned answers; assert the marshaled return object — exercises `marshalDomains` (single vs array, dedupe via Set, extraHosts) and `marshalWordPress` (boolean→{}, title/user/pass/email, type, purify) without touching source | ### `test/integration/` — real module logic with mocked dependencies diff --git a/progress.txt b/progress.txt index 617ed6f4..3d3d4852 100644 --- a/progress.txt +++ b/progress.txt @@ -14,3 +14,8 @@ 2026-06-05: env-utils unit tests (pure subset) - Created test/unit/env-utils.test.js: 13 passing tests for envSlug (slugify behavior) and createDefaultProxy (http prefix, .com TLD handling) - npm test: 56/56 pass; npm run lint: clean + +2026-06-05: configure unit tests (pure subset) +- Created test/unit/configure.test.js: 12 passing tests for getConfigDirectory, getGlobalDirectory, getDefaults (path composition under mocked os.homedir), and createProxyConfig (#{TRY_PROXY}/#{PROXY_URL} substitution, multi-placeholder, passthrough) +- Mocked os.homedir() via jest.mock('os', ...) at the test-file level; getConfigFilePath is not exported so not tested directly +- npm test: 68/68 pass; npm run lint: clean diff --git a/test/unit/configure.test.js b/test/unit/configure.test.js new file mode 100644 index 00000000..6aba0e8c --- /dev/null +++ b/test/unit/configure.test.js @@ -0,0 +1,81 @@ +'use strict'; + +const path = require( 'path' ); + +jest.mock( 'os', () => ( { + ...jest.requireActual( 'os' ), + homedir: () => '/fake/home', +} ) ); + +const { + createProxyConfig, + getDefaults, + getConfigDirectory, + getGlobalDirectory, +} = require( '../../src/configure' ); + +describe( 'getConfigDirectory', () => { + it( 'returns .wplocaldocker inside the home directory', () => { + expect( getConfigDirectory() ).toBe( path.join( '/fake/home', '.wplocaldocker' ) ); + } ); +} ); + +describe( 'getGlobalDirectory', () => { + it( 'returns global/ inside the config directory', () => { + expect( getGlobalDirectory() ).toBe( path.join( '/fake/home', '.wplocaldocker', 'global' ) ); + } ); +} ); + +describe( 'getDefaults', () => { + it( 'sitesPath is inside the home directory', () => { + expect( getDefaults().sitesPath ).toBe( path.join( '/fake/home', 'wp-local-docker-sites' ) ); + } ); + + it( 'snapshotsPath is inside the home directory', () => { + expect( getDefaults().snapshotsPath ).toBe( path.join( '/fake/home', '.wpsnapshots' ) ); + } ); + + it( 'manageHosts defaults to true', () => { + expect( getDefaults().manageHosts ).toBe( true ); + } ); + + it( 'overwriteGlobal defaults to true', () => { + expect( getDefaults().overwriteGlobal ).toBe( true ); + } ); +} ); + +describe( 'createProxyConfig', () => { + it( 'replaces #{TRY_PROXY} with the try_files directive', () => { + const result = createProxyConfig( 'https://example.com', '#{TRY_PROXY}' ); + expect( result ).toBe( 'try_files $uri @production;' ); + } ); + + it( 'replaces #{PROXY_URL} with a location block', () => { + const result = createProxyConfig( 'https://example.com', '#{PROXY_URL}' ); + expect( result ).toContain( 'location @production {' ); + expect( result ).toContain( '}' ); + } ); + + it( 'injects the proxy URL into the proxy_pass directive', () => { + const result = createProxyConfig( 'https://example.com', '#{PROXY_URL}' ); + expect( result ).toContain( 'proxy_pass https://example.com/$uri;' ); + } ); + + it( 'replaces both placeholders in the same config string', () => { + const input = 'before #{TRY_PROXY} middle #{PROXY_URL} after'; + const result = createProxyConfig( 'https://example.com', input ); + expect( result ).toContain( 'try_files $uri @production;' ); + expect( result ).toContain( 'location @production {' ); + expect( result ).toContain( 'after' ); + } ); + + it( 'leaves a string without placeholders unchanged', () => { + const input = 'server { listen 80; }'; + expect( createProxyConfig( 'https://example.com', input ) ).toBe( input ); + } ); + + it( 'uses different proxy URLs in the generated block', () => { + const result = createProxyConfig( 'https://staging.example.com', '#{PROXY_URL}' ); + expect( result ).toContain( 'proxy_pass https://staging.example.com/$uri;' ); + } ); +} ); From f0b5812ecd18378f3b99bd86aca6442edfc7b16c Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Fri, 5 Jun 2026 11:52:06 +0000 Subject: [PATCH 05/16] test: add create/inquirer unit tests via stub prompt 21 tests covering marshalDomains (dedup, extraHosts, scalar/array), marshalWordPress (boolean expansion, type, purify, field mapping), and top-level field passthrough (php, name, elasticsearch, mediaProxy). Co-Authored-By: Claude Sonnet 4.6 --- prd.md | 2 +- progress.txt | 5 + test/unit/create/inquirer.test.js | 254 ++++++++++++++++++++++++++++++ 3 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 test/unit/create/inquirer.test.js diff --git a/prd.md b/prd.md index c19e363e..b94dcd8e 100644 --- a/prd.md +++ b/prd.md @@ -73,7 +73,7 @@ Decisions confirmed with the user: **Jest**, **comprehensive** scope, **wire int | ✅ `prompt-validators.test.js` | `src/prompt-validators.js` | `validateNotEmpty` (empty/whitespace/valid), `validateBool` (y/yes/n/no/other/non-string passthrough), `parseHostname` (strips `http(s)://`, spaces, path), `parseProxyUrl` (adds protocol, trims slashes) — 43 tests total across both unit files (2026-06-05) | | ✅ `env-utils.test.js` (pure subset) | `src/env-utils.js` | `envSlug` (slugify behavior), `createDefaultProxy` (adds `http://`, `.com` TLD handling) — 13 tests (2026-06-05) | | ✅ `configure.test.js` (pure subset) | `src/configure.js` | `createProxyConfig` (placeholder `#{TRY_PROXY}`/`#{PROXY_URL}` substitution, multi-placeholder, no-placeholder passthrough), `getDefaults` (paths + booleans), `getConfigDirectory`/`getGlobalDirectory` (path composition under a stubbed `os.homedir`) — 12 tests (2026-06-05) | -| `create/inquirer.test.js` | `src/commands/create/inquirer.js` | Call `makeInquirer({ prompt })` with a **stub prompt** returning canned answers; assert the marshaled return object — exercises `marshalDomains` (single vs array, dedupe via Set, extraHosts) and `marshalWordPress` (boolean→{}, title/user/pass/email, type, purify) without touching source | +| ✅ `create/inquirer.test.js` | `src/commands/create/inquirer.js` | Call `makeInquirer({ prompt })` with a **stub prompt** returning canned answers; assert the marshaled return object — exercises `marshalDomains` (single vs array, dedupe via Set, extraHosts) and `marshalWordPress` (boolean→{}, title/user/pass/email, type, purify) without touching source — 21 tests (2026-06-05) | ### `test/integration/` — real module logic with mocked dependencies diff --git a/progress.txt b/progress.txt index 3d3d4852..fd14f301 100644 --- a/progress.txt +++ b/progress.txt @@ -19,3 +19,8 @@ - Created test/unit/configure.test.js: 12 passing tests for getConfigDirectory, getGlobalDirectory, getDefaults (path composition under mocked os.homedir), and createProxyConfig (#{TRY_PROXY}/#{PROXY_URL} substitution, multi-placeholder, passthrough) - Mocked os.homedir() via jest.mock('os', ...) at the test-file level; getConfigFilePath is not exported so not tested directly - npm test: 68/68 pass; npm run lint: clean + +2026-06-05: create/inquirer unit tests +- Created test/unit/create/inquirer.test.js: 21 passing tests for makeInquirer via stub prompt +- Covers marshalDomains (single hostname, scalar domain passthrough, Set deduplication, extraHosts merging), marshalWordPress (boolean→{}, title/user/pass/email, type, purify, emptyContent), and top-level field passthrough (php, name, elasticsearch, mediaProxy) +- npm test: 89/89 pass; npm run lint: clean diff --git a/test/unit/create/inquirer.test.js b/test/unit/create/inquirer.test.js new file mode 100644 index 00000000..41bedee2 --- /dev/null +++ b/test/unit/create/inquirer.test.js @@ -0,0 +1,254 @@ +'use strict'; + +const makeInquirer = require( '../../../src/commands/create/inquirer' ); + +// Helper: returns a stub prompt that resolves with `answers` immediately. +function stubPrompt( answers ) { + return jest.fn().mockResolvedValue( answers ); +} + +describe( 'makeInquirer — marshalDomains (single hostname)', () => { + it( 'returns the hostname string when no existing domain is set', async () => { + const prompt = stubPrompt( { hostname: 'docker.test', phpVersion: '8.2' } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( {} ); + expect( result.domain ).toBe( 'docker.test' ); + } ); + + it( 'passes the existing scalar domain through unchanged', async () => { + const prompt = stubPrompt( { phpVersion: '8.2' } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( { domain: 'existing.test' } ); + expect( result.domain ).toBe( 'existing.test' ); + } ); + + it( 'deduplicates when hostname matches a domain already in the defaults array', async () => { + const prompt = stubPrompt( { hostname: 'a.test', phpVersion: '8.2' } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( { domain: 'a.test' } ); + // domain is already 'a.test' (scalar), hostname == same → Set dedupes → single string + expect( result.domain ).toBe( 'a.test' ); + } ); +} ); + +describe( 'makeInquirer — marshalDomains (extra hosts)', () => { + it( 'merges extraHosts into the domain array', async () => { + const prompt = stubPrompt( { + hostname: 'main.test', + extraHosts: [ 'alias1.test', 'alias2.test' ], + phpVersion: '8.2', + } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( {} ); + expect( result.domain ).toEqual( [ 'main.test', 'alias1.test', 'alias2.test' ] ); + } ); + + it( 'deduplicates repeated extra hosts', async () => { + const prompt = stubPrompt( { + hostname: 'main.test', + extraHosts: [ 'main.test', 'alias.test' ], + phpVersion: '8.2', + } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( {} ); + // 'main.test' added by hostname, 'main.test' in extraHosts is duped → Set removes it + expect( result.domain ).toEqual( [ 'main.test', 'alias.test' ] ); + } ); + + it( 'combines an existing scalar domain with extraHosts', async () => { + const prompt = stubPrompt( { + extraHosts: [ 'extra.test' ], + phpVersion: '8.2', + } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( { domain: 'existing.test' } ); + expect( result.domain ).toEqual( [ 'existing.test', 'extra.test' ] ); + } ); + + it( 'combines an existing array domain with a new hostname and extraHosts', async () => { + const prompt = stubPrompt( { + hostname: 'new.test', + extraHosts: [ 'extra.test' ], + phpVersion: '8.2', + } ); + const inquirer = makeInquirer( { prompt } ); + // domain is an empty array, so hostname/extraHosts questions fire + const result = await inquirer( { domain: [] } ); + expect( result.domain ).toEqual( [ 'new.test', 'extra.test' ] ); + } ); +} ); + +describe( 'makeInquirer — marshalWordPress (boolean true → {})', () => { + it( 'expands wordpress:true into an object with title/username/password/email', async () => { + const prompt = stubPrompt( { + hostname: 'site.test', + wordpress: true, + wordpressType: 'single', + title: 'My Site', + username: 'admin', + password: 'secret', + email: 'admin@example.com', + phpVersion: '8.2', + } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( {} ); + expect( result.wordpress ).toEqual( { + type: 'single', + title: 'My Site', + username: 'admin', + password: 'secret', + email: 'admin@example.com', + } ); + } ); + + it( 'sets purify:true when emptyContent is true', async () => { + const prompt = stubPrompt( { + hostname: 'site.test', + wordpress: true, + wordpressType: 'single', + title: 'My Site', + username: 'admin', + password: 'secret', + email: 'admin@example.com', + emptyContent: true, + phpVersion: '8.2', + } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( {} ); + expect( result.wordpress.purify ).toBe( true ); + } ); + + it( 'does not set purify when emptyContent is false', async () => { + const prompt = stubPrompt( { + hostname: 'site.test', + wordpress: true, + wordpressType: 'single', + title: 'My Site', + username: 'admin', + password: 'secret', + email: 'admin@example.com', + emptyContent: false, + phpVersion: '8.2', + } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( {} ); + expect( result.wordpress.purify ).toBeUndefined(); + } ); + + it( 'maps wordpressType subdirectory correctly', async () => { + const prompt = stubPrompt( { + hostname: 'ms.test', + wordpress: true, + wordpressType: 'subdirectory', + title: 'MS', + username: 'admin', + password: 'password', + email: 'a@b.com', + phpVersion: '8.2', + } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( {} ); + expect( result.wordpress.type ).toBe( 'subdirectory' ); + } ); + + it( 'maps wordpressType subdomain correctly', async () => { + const prompt = stubPrompt( { + hostname: 'ms.test', + wordpress: true, + wordpressType: 'subdomain', + title: 'MS', + username: 'admin', + password: 'password', + email: 'a@b.com', + phpVersion: '8.2', + } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( {} ); + expect( result.wordpress.type ).toBe( 'subdomain' ); + } ); +} ); + +describe( 'makeInquirer — marshalWordPress (existing object defaults)', () => { + it( 'merges answers into a pre-existing wordpress object', async () => { + const prompt = stubPrompt( { + title: 'New Title', + username: 'newuser', + password: 'newpass', + email: 'new@e.com', + phpVersion: '8.2', + } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( { + wordpress: { type: 'single', title: undefined }, + } ); + expect( result.wordpress.title ).toBe( 'New Title' ); + expect( result.wordpress.username ).toBe( 'newuser' ); + } ); +} ); + +describe( 'makeInquirer — top-level field passthrough', () => { + it( 'uses the php version from defaults when already set', async () => { + const prompt = stubPrompt( { hostname: 'site.test' } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( { php: '8.1' } ); + expect( result.php ).toBe( '8.1' ); + } ); + + it( 'uses phpVersion from answers when php not in defaults', async () => { + const prompt = stubPrompt( { hostname: 'site.test', phpVersion: '7.4' } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( {} ); + expect( result.php ).toBe( '7.4' ); + } ); + + it( 'uses the name from defaults when set', async () => { + const prompt = stubPrompt( { hostname: 'site.test', phpVersion: '8.2' } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( { name: 'my-project' } ); + expect( result.name ).toBe( 'my-project' ); + } ); + + it( 'falls back to answers.title for name when not in defaults', async () => { + const prompt = stubPrompt( { + hostname: 'site.test', + title: 'Derived Name', + wordpress: true, + wordpressType: 'single', + username: 'admin', + password: 'pass', + email: 'a@b.com', + phpVersion: '8.2', + } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( {} ); + expect( result.name ).toBe( 'Derived Name' ); + } ); + + it( 'sets elasticsearch from answers when not in defaults', async () => { + const prompt = stubPrompt( { hostname: 'site.test', phpVersion: '8.2', elasticsearch: true } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( {} ); + expect( result.elasticsearch ).toBe( true ); + } ); + + it( 'defaults elasticsearch to false when neither defaults nor answers set it', async () => { + const prompt = stubPrompt( { hostname: 'site.test', phpVersion: '8.2' } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( {} ); + expect( result.elasticsearch ).toBe( false ); + } ); + + it( 'sets mediaProxy to the proxy answer when present', async () => { + const prompt = stubPrompt( { hostname: 'site.test', phpVersion: '8.2', proxy: 'http://proxy.example.com' } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( {} ); + expect( result.mediaProxy ).toBe( 'http://proxy.example.com' ); + } ); + + it( 'sets mediaProxy to false when proxy answer is absent', async () => { + const prompt = stubPrompt( { hostname: 'site.test', phpVersion: '8.2' } ); + const inquirer = makeInquirer( { prompt } ); + const result = await inquirer( {} ); + expect( result.mediaProxy ).toBe( false ); + } ); +} ); From 9bb433062dd859935c13808cffb8bd58e4430c22 Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Fri, 5 Jun 2026 11:54:59 +0000 Subject: [PATCH 06/16] test: add make-docker-compose integration tests 17 tests covering PHP image selection, certs/CERT_NAME, VIRTUAL_HOST wildcards, wordpress dev/non-dev nginx+wp-cli config, elasticsearch on/off, Linux vs non-Linux branch (custom image, build args, volumes), cacheVolume presence, dockerCompose filter hook, spinner calls, and a full-config snapshot. Co-Authored-By: Claude Sonnet 4.6 --- prd.md | 2 +- progress.txt | 6 + .../make-docker-compose.test.js.snap | 77 ++++++ .../create/make-docker-compose.test.js | 221 ++++++++++++++++++ 4 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 test/integration/create/__snapshots__/make-docker-compose.test.js.snap create mode 100644 test/integration/create/make-docker-compose.test.js diff --git a/prd.md b/prd.md index b94dcd8e..07d64d5a 100644 --- a/prd.md +++ b/prd.md @@ -79,7 +79,7 @@ Decisions confirmed with the user: **Jest**, **comprehensive** scope, **wire int | File | Module under test | Mocks | Focus | | ------------------------------------ | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `create/make-docker-compose.test.js` | `src/commands/create/make-docker-compose.js` | `../../configure` (`config.get('snapshotsPath')`), `os.platform`, `process.env.USER`/`process.getuid` for the Linux branch | The generated compose object across matrix: PHP version → correct `images[...]`; `wordpress.type` `dev` vs other → nginx conf + wp-cli volume; `elasticsearch` on/off → service+volume; `certs` on/off → `CERT_NAME`; **Linux vs non-Linux** branch (build args, ssh/aws/wpsnapshots volumes); the `settings.dockerCompose` filter hook. Use a **snapshot** for the full object plus targeted assertions. | +| ✅ `create/make-docker-compose.test.js` | `src/commands/create/make-docker-compose.js` | `../../configure` (`config.get('snapshotsPath')`), `os.platform`, `process.env.USER`/`process.getuid` for the Linux branch | The generated compose object across matrix: PHP version → correct `images[...]`; `wordpress.type` `dev` vs other → nginx conf + wp-cli volume; `elasticsearch` on/off → service+volume; `certs` on/off → `CERT_NAME`; **Linux vs non-Linux** branch (build args, ssh/aws/wpsnapshots volumes); the `settings.dockerCompose` filter hook. Snapshot for full object plus targeted assertions — 17 tests (2026-06-05) | | `utils/docker-compose.test.js` | `src/utils/docker-compose.js` | `docker-compose` npm package | Each proxied method (`down/exec/logs/ps/pullAll/restartAll/run/upAll`) forwards args and returns `out`; throws `err` when `exitCode` is truthy (`interpretComposerResults`); `isRunning` → true when `port` resolves, false when it rejects | | `environment.test.js` | `src/environment.js` | `./configure`, `./database`, `./env-utils`, `./gateway`, `./utils/docker-compose`, `fs-extra`, `inquirer`, `which`, `@vscode/sudo-prompt` | Orchestration & branching: `start` (pull vs no-pull, calls `gateway.startGlobal` then `compose.upAll`), `stop`, `restart` (`isRunning` true→down first), `deleteEnv` (confirm=false short-circuits; confirm=true removes files/certs/db; `manageHosts` true→sudo hosts removal), `startAll/stopAll/restartAll/deleteAll` iterate envs. Assert call order/args, not Docker behavior. | | `gateway.test.js` | `src/gateway.js` | `./utils/make-docker` (mock docker w/ `getNetwork`/`getVolume`/`createNetwork`/`createVolume`), `./utils/docker-compose`, `./configure`, `fs`, `netcat/client` | `ensureNetworkExists` (creates when `inspect` rejects, skips when it resolves), `removeNetwork`, `ensureCacheExists`/`removeCacheVolume`, `startGlobal` idempotency via the module-level `started` flag, `waitForDB` (fake timers + mock netcat emitting `data`). Use `jest.resetModules()` per test to reset `started`. | diff --git a/progress.txt b/progress.txt index fd14f301..f64dfac0 100644 --- a/progress.txt +++ b/progress.txt @@ -20,6 +20,12 @@ - Mocked os.homedir() via jest.mock('os', ...) at the test-file level; getConfigFilePath is not exported so not tested directly - npm test: 68/68 pass; npm run lint: clean +2026-06-05: create/make-docker-compose integration tests +- Created test/integration/create/make-docker-compose.test.js: 17 passing tests +- Mocked ../../configure (config.get → SNAPSHOTS_PATH) and os.platform (darwin/linux) +- Covers: PHP version → image, certs on/off → CERT_NAME, VIRTUAL_HOST wildcards, wordpress.type dev/non-dev → nginx+wp-cli volumes, elasticsearch on/off, Linux branch (custom image, build args, user-scoped volumes), cacheVolume, dockerCompose filter hook (truthy/falsy return), spinner.start/succeed, snapshot for full non-linux standard config +- npm test: 106/106 pass; npm run lint: clean + 2026-06-05: create/inquirer unit tests - Created test/unit/create/inquirer.test.js: 21 passing tests for makeInquirer via stub prompt - Covers marshalDomains (single hostname, scalar domain passthrough, Set deduplication, extraHosts merging), marshalWordPress (boolean→{}, title/user/pass/email, type, purify, emptyContent), and top-level field passthrough (php, name, elasticsearch, mediaProxy) diff --git a/test/integration/create/__snapshots__/make-docker-compose.test.js.snap b/test/integration/create/__snapshots__/make-docker-compose.test.js.snap new file mode 100644 index 00000000..0969b6bf --- /dev/null +++ b/test/integration/create/__snapshots__/make-docker-compose.test.js.snap @@ -0,0 +1,77 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`makeDockerCompose matches snapshot for a standard non-linux environment 1`] = ` +{ + "networks": { + "wplocaldocker": { + "external": true, + "name": "wplocaldocker", + }, + }, + "services": { + "memcached": { + "image": "memcached:latest", + }, + "nginx": { + "depends_on": [ + "phpfpm", + ], + "environment": { + "CERT_NAME": "mysite-test", + "HTTPS_METHOD": "noredirect", + "VIRTUAL_HOST": "mysite.test,*.mysite.test", + }, + "expose": [ + "80", + "443", + ], + "image": "nginx:latest", + "networks": [ + "default", + "wplocaldocker", + ], + "volumes": [ + "./wordpress:/var/www/html:cached", + "./config/nginx/server.conf:/etc/nginx/conf.d/common/_server.conf:cached", + "./config/nginx/default.conf:/etc/nginx/conf.d/default.conf:cached", + ], + }, + "phpfpm": { + "cap_add": [ + "SYS_PTRACE", + ], + "depends_on": [ + "memcached", + ], + "dns": [ + "10.0.0.2", + ], + "environment": { + "ENABLE_XDEBUG": "true", + "PHP_IDE_CONFIG": "serverName=mysite-test", + }, + "image": "10up/wp-php-fpm-dev:8.2-ubuntu", + "networks": [ + "default", + "wplocaldocker", + ], + "volumes": [ + "./wordpress:/var/www/html:cached", + "./config/php-fpm/docker-php-ext-xdebug.ini:/etc/php/8.2/fpm/conf.d/docker-php-ext-xdebug.ini:cached", + "wplocaldockerCache:/var/www/.wp-cli/cache:cached", + "~/.ssh:/home/www-data/.ssh:cached", + "/home/testuser/.wpsnapshots:/home/www-data/.wpsnapshots:cached", + "~/.aws:/home/www-data/.aws:cached,ro", + "./config/php-fpm/wp-cli.local.yml:/var/www/.wp-cli/config.yml:cached", + ], + }, + }, + "version": "2.2", + "volumes": { + "wplocaldockerCache": { + "external": true, + "name": "wplocaldockerCache", + }, + }, +} +`; diff --git a/test/integration/create/make-docker-compose.test.js b/test/integration/create/make-docker-compose.test.js new file mode 100644 index 00000000..8d312ffd --- /dev/null +++ b/test/integration/create/make-docker-compose.test.js @@ -0,0 +1,221 @@ +'use strict'; + +jest.mock( '../../../src/configure' ); + +// Variable must be prefixed with 'mock' to be accessible inside jest.mock factory +const mockOs = { + platform: jest.fn(), +}; +jest.mock( 'os', () => mockOs ); + +const { images } = require( '../../../src/docker-images' ); +const { cacheVolume } = require( '../../../src/env-utils' ); + +const SNAPSHOTS_PATH = '/home/testuser/.wpsnapshots'; + +describe( 'makeDockerCompose', () => { + let makeDockerCompose; + let mockConfig; + + beforeEach( () => { + jest.resetModules(); + + // Re-require after resetModules so mocks are fresh + jest.mock( '../../../src/configure' ); + mockConfig = require( '../../../src/configure' ); + mockConfig.get = jest.fn().mockResolvedValue( SNAPSHOTS_PATH ); + + // Default to non-linux platform + mockOs.platform.mockReturnValue( 'darwin' ); + + // After mocks are set up, require the module under test + makeDockerCompose = require( '../../../src/commands/create/make-docker-compose' ); + } ); + + function baseSettings( overrides = {} ) { + return { + envSlug: 'mysite-test', + php: '8.2', + wordpress: { type: 'standard' }, + elasticsearch: false, + certs: false, + ...overrides, + }; + } + + const baseHosts = [ 'mysite.test' ]; + + // ----------------------------------------------------------------------- + // PHP version → correct image + // ----------------------------------------------------------------------- + it( 'uses the correct phpfpm image for php8.2', async () => { + const fn = makeDockerCompose( null ); + const result = await fn( baseHosts, baseSettings( { php: '8.2' } ) ); + expect( result.services.phpfpm.image ).toBe( images['php8.2'] ); + } ); + + it( 'uses the correct phpfpm image for php7.4', async () => { + const fn = makeDockerCompose( null ); + const result = await fn( baseHosts, baseSettings( { php: '7.4' } ) ); + expect( result.services.phpfpm.image ).toBe( images['php7.4'] ); + } ); + + // ----------------------------------------------------------------------- + // CERT_NAME: certs on/off + // ----------------------------------------------------------------------- + it( 'sets CERT_NAME to envSlug when certs is true', async () => { + const fn = makeDockerCompose( null ); + const result = await fn( baseHosts, baseSettings( { certs: true, envSlug: 'mysite-test' } ) ); + expect( result.services.nginx.environment.CERT_NAME ).toBe( 'mysite-test' ); + } ); + + it( 'sets CERT_NAME to localhost when certs is false', async () => { + const fn = makeDockerCompose( null ); + const result = await fn( baseHosts, baseSettings( { certs: false } ) ); + expect( result.services.nginx.environment.CERT_NAME ).toBe( 'localhost' ); + } ); + + // ----------------------------------------------------------------------- + // VIRTUAL_HOST includes wildcard hosts + // ----------------------------------------------------------------------- + it( 'sets VIRTUAL_HOST to host + wildcard', async () => { + const fn = makeDockerCompose( null ); + const result = await fn( [ 'mysite.test' ], baseSettings() ); + expect( result.services.nginx.environment.VIRTUAL_HOST ).toBe( 'mysite.test,*.mysite.test' ); + } ); + + // ----------------------------------------------------------------------- + // wordpress type: dev branch vs non-dev + // ----------------------------------------------------------------------- + it( 'uses develop.conf nginx config and wp-cli develop volume when type is dev', async () => { + const fn = makeDockerCompose( null ); + const result = await fn( baseHosts, baseSettings( { wordpress: { type: 'dev' } } ) ); + const nginxVolumes = result.services.nginx.volumes; + const phpfpmVolumes = result.services.phpfpm.volumes; + expect( nginxVolumes.some( ( v ) => v.includes( 'develop.conf' ) ) ).toBe( true ); + expect( phpfpmVolumes.some( ( v ) => v.includes( 'wp-cli.develop.yml' ) ) ).toBe( true ); + } ); + + it( 'uses default.conf nginx config and wp-cli local volume when type is not dev', async () => { + const fn = makeDockerCompose( null ); + const result = await fn( baseHosts, baseSettings( { wordpress: { type: 'standard' } } ) ); + const nginxVolumes = result.services.nginx.volumes; + const phpfpmVolumes = result.services.phpfpm.volumes; + expect( nginxVolumes.some( ( v ) => v.includes( 'default.conf' ) ) ).toBe( true ); + expect( phpfpmVolumes.some( ( v ) => v.includes( 'wp-cli.local.yml' ) ) ).toBe( true ); + } ); + + // ----------------------------------------------------------------------- + // Elasticsearch: on/off + // ----------------------------------------------------------------------- + it( 'does not add elasticsearch service when elasticsearch is false', async () => { + const fn = makeDockerCompose( null ); + const result = await fn( baseHosts, baseSettings( { elasticsearch: false } ) ); + expect( result.services.elasticsearch ).toBeUndefined(); + expect( result.volumes.elasticsearchData ).toBeUndefined(); + } ); + + it( 'adds elasticsearch service and volume when elasticsearch is true', async () => { + const fn = makeDockerCompose( null ); + const result = await fn( baseHosts, baseSettings( { elasticsearch: true } ) ); + expect( result.services.elasticsearch ).toBeDefined(); + expect( result.services.elasticsearch.image ).toBe( images['elasticsearch'] ); + expect( result.services.elasticsearch.expose ).toEqual( [ '9200' ] ); + expect( result.volumes.elasticsearchData ).toBeDefined(); + expect( result.services.phpfpm.depends_on ).toContain( 'elasticsearch' ); + } ); + + // ----------------------------------------------------------------------- + // Non-Linux branch: wpsnapshots volume uses www-data + // ----------------------------------------------------------------------- + it( 'mounts wpsnapshots under /home/www-data on non-linux', async () => { + mockOs.platform.mockReturnValue( 'darwin' ); + const fn = makeDockerCompose( null ); + const result = await fn( baseHosts, baseSettings() ); + const vols = result.services.phpfpm.volumes; + expect( vols.some( ( v ) => v.includes( `${ SNAPSHOTS_PATH }:/home/www-data/.wpsnapshots` ) ) ).toBe( true ); + } ); + + // ----------------------------------------------------------------------- + // Linux branch: custom image, build args, user-scoped volumes + // ----------------------------------------------------------------------- + it( 'uses custom image and build args on linux', async () => { + mockOs.platform.mockReturnValue( 'linux' ); + const savedUser = process.env.USER; + const savedGetuid = process.getuid; + process.env.USER = 'devuser'; + process.getuid = () => 1001; + + const fn = makeDockerCompose( null ); + const result = await fn( baseHosts, baseSettings( { php: '8.2' } ) ); + + expect( result.services.phpfpm.image ).toBe( 'wp-php-fpm-dev-8.2-devuser' ); + expect( result.services.phpfpm.build.args.CALLING_USER ).toBe( 'devuser' ); + expect( result.services.phpfpm.build.args.CALLING_UID ).toBe( 1001 ); + expect( result.services.phpfpm.build.args.PHP_IMAGE ).toBe( images['php8.2'] ); + expect( result.services.phpfpm.volumes.some( ( v ) => v.includes( '/home/devuser/.wpsnapshots' ) ) ).toBe( true ); + + process.env.USER = savedUser; + process.getuid = savedGetuid; + } ); + + // ----------------------------------------------------------------------- + // cacheVolume in phpfpm volumes and named volumes section + // ----------------------------------------------------------------------- + it( 'includes cacheVolume in phpfpm volumes and top-level volumes', async () => { + const fn = makeDockerCompose( null ); + const result = await fn( baseHosts, baseSettings() ); + expect( result.services.phpfpm.volumes.some( ( v ) => v.startsWith( `${ cacheVolume }:` ) ) ).toBe( true ); + expect( result.volumes[ cacheVolume ] ).toBeDefined(); + expect( result.volumes[ cacheVolume ].name ).toBe( cacheVolume ); + } ); + + // ----------------------------------------------------------------------- + // dockerCompose filter hook + // ----------------------------------------------------------------------- + it( 'applies dockerCompose filter when it is a function and returns a value', async () => { + const customConfig = { custom: true }; + const settings = baseSettings( { + dockerCompose: jest.fn().mockResolvedValue( customConfig ), + } ); + const fn = makeDockerCompose( null ); + const result = await fn( baseHosts, settings ); + expect( result ).toEqual( { custom: true } ); + expect( settings.dockerCompose ).toHaveBeenCalled(); + } ); + + it( 'keeps original config when dockerCompose filter returns falsy', async () => { + const settings = baseSettings( { + dockerCompose: jest.fn().mockResolvedValue( null ), + } ); + const fn = makeDockerCompose( null ); + const result = await fn( baseHosts, settings ); + expect( result.services.nginx ).toBeDefined(); + } ); + + // ----------------------------------------------------------------------- + // Spinner path: called with spinner calls start/succeed + // ----------------------------------------------------------------------- + it( 'calls spinner.start and spinner.succeed when spinner is provided', async () => { + const spinner = { start: jest.fn(), succeed: jest.fn() }; + const fn = makeDockerCompose( spinner ); + await fn( baseHosts, baseSettings() ); + expect( spinner.start ).toHaveBeenCalledTimes( 1 ); + expect( spinner.succeed ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'does not throw when spinner is null', async () => { + const fn = makeDockerCompose( null ); + await expect( fn( baseHosts, baseSettings() ) ).resolves.toBeDefined(); + } ); + + // ----------------------------------------------------------------------- + // Snapshot: full non-linux standard config + // ----------------------------------------------------------------------- + it( 'matches snapshot for a standard non-linux environment', async () => { + mockOs.platform.mockReturnValue( 'darwin' ); + const fn = makeDockerCompose( null ); + const result = await fn( [ 'mysite.test' ], baseSettings( { certs: true, envSlug: 'mysite-test', php: '8.2' } ) ); + expect( result ).toMatchSnapshot(); + } ); +} ); From d31903658d252608bc059c43ecdf5d37a2b9b878 Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Fri, 5 Jun 2026 11:56:22 +0000 Subject: [PATCH 07/16] test: add utils/docker-compose integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 18 tests covering all 8 proxied methods (arg forwarding, exitCode error throwing) and isRunning (port resolve→true, port reject→false). Co-Authored-By: Claude Sonnet 4.6 --- prd.md | 2 +- progress.txt | 5 ++ test/integration/utils/docker-compose.test.js | 71 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 test/integration/utils/docker-compose.test.js diff --git a/prd.md b/prd.md index 07d64d5a..e54d2c69 100644 --- a/prd.md +++ b/prd.md @@ -80,7 +80,7 @@ Decisions confirmed with the user: **Jest**, **comprehensive** scope, **wire int | File | Module under test | Mocks | Focus | | ------------------------------------ | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ✅ `create/make-docker-compose.test.js` | `src/commands/create/make-docker-compose.js` | `../../configure` (`config.get('snapshotsPath')`), `os.platform`, `process.env.USER`/`process.getuid` for the Linux branch | The generated compose object across matrix: PHP version → correct `images[...]`; `wordpress.type` `dev` vs other → nginx conf + wp-cli volume; `elasticsearch` on/off → service+volume; `certs` on/off → `CERT_NAME`; **Linux vs non-Linux** branch (build args, ssh/aws/wpsnapshots volumes); the `settings.dockerCompose` filter hook. Snapshot for full object plus targeted assertions — 17 tests (2026-06-05) | -| `utils/docker-compose.test.js` | `src/utils/docker-compose.js` | `docker-compose` npm package | Each proxied method (`down/exec/logs/ps/pullAll/restartAll/run/upAll`) forwards args and returns `out`; throws `err` when `exitCode` is truthy (`interpretComposerResults`); `isRunning` → true when `port` resolves, false when it rejects | +| ✅ `utils/docker-compose.test.js` | `src/utils/docker-compose.js` | `docker-compose` npm package | Each proxied method (`down/exec/logs/ps/pullAll/restartAll/run/upAll`) forwards args and returns `out`; throws `err` when `exitCode` is truthy (`interpretComposerResults`); `isRunning` → true when `port` resolves, false when it rejects — 18 tests (2026-06-05) | | `environment.test.js` | `src/environment.js` | `./configure`, `./database`, `./env-utils`, `./gateway`, `./utils/docker-compose`, `fs-extra`, `inquirer`, `which`, `@vscode/sudo-prompt` | Orchestration & branching: `start` (pull vs no-pull, calls `gateway.startGlobal` then `compose.upAll`), `stop`, `restart` (`isRunning` true→down first), `deleteEnv` (confirm=false short-circuits; confirm=true removes files/certs/db; `manageHosts` true→sudo hosts removal), `startAll/stopAll/restartAll/deleteAll` iterate envs. Assert call order/args, not Docker behavior. | | `gateway.test.js` | `src/gateway.js` | `./utils/make-docker` (mock docker w/ `getNetwork`/`getVolume`/`createNetwork`/`createVolume`), `./utils/docker-compose`, `./configure`, `fs`, `netcat/client` | `ensureNetworkExists` (creates when `inspect` rejects, skips when it resolves), `removeNetwork`, `ensureCacheExists`/`removeCacheVolume`, `startGlobal` idempotency via the module-level `started` flag, `waitForDB` (fake timers + mock netcat emitting `data`). Use `jest.resetModules()` per test to reset `started`. | | `database.test.js` | `src/database.js` | `mysql` (mock `createConnection` → `{ query(sql, cb), destroy() }`) | `create`/`deleteDatabase`/`assignPrivs` emit the correct SQL, `destroy()` the connection, resolve on success, reject on query error | diff --git a/progress.txt b/progress.txt index f64dfac0..4dd00c17 100644 --- a/progress.txt +++ b/progress.txt @@ -26,6 +26,11 @@ - Covers: PHP version → image, certs on/off → CERT_NAME, VIRTUAL_HOST wildcards, wordpress.type dev/non-dev → nginx+wp-cli volumes, elasticsearch on/off, Linux branch (custom image, build args, user-scoped volumes), cacheVolume, dockerCompose filter hook (truthy/falsy return), spinner.start/succeed, snapshot for full non-linux standard config - npm test: 106/106 pass; npm run lint: clean +2026-06-05: utils/docker-compose integration tests +- Created test/integration/utils/docker-compose.test.js: 18 passing tests +- Mocked docker-compose npm package entirely; all 8 proxied methods tested for arg-forwarding and exitCode error throwing via interpretComposerResults; isRunning tested for resolve→true and reject→false +- npm test: 124/124 pass; npm run lint: clean + 2026-06-05: create/inquirer unit tests - Created test/unit/create/inquirer.test.js: 21 passing tests for makeInquirer via stub prompt - Covers marshalDomains (single hostname, scalar domain passthrough, Set deduplication, extraHosts merging), marshalWordPress (boolean→{}, title/user/pass/email, type, purify, emptyContent), and top-level field passthrough (php, name, elasticsearch, mediaProxy) diff --git a/test/integration/utils/docker-compose.test.js b/test/integration/utils/docker-compose.test.js new file mode 100644 index 00000000..94d68fd5 --- /dev/null +++ b/test/integration/utils/docker-compose.test.js @@ -0,0 +1,71 @@ +'use strict'; + +const mockCompose = { + down: jest.fn(), + exec: jest.fn(), + logs: jest.fn(), + ps: jest.fn(), + pullAll: jest.fn(), + restartAll: jest.fn(), + run: jest.fn(), + upAll: jest.fn(), + port: jest.fn(), +}; + +jest.mock( 'docker-compose', () => mockCompose ); + +const dc = require( '../../../src/utils/docker-compose' ); + +function resolvesWith( out ) { + return Promise.resolve( { out, err: '', exitCode: 0 } ); +} + +function rejectsWith( err ) { + return Promise.resolve( { out: '', err, exitCode: 1 } ); +} + +const PROXIED_METHODS = [ 'down', 'exec', 'logs', 'ps', 'pullAll', 'restartAll', 'run', 'upAll' ]; + +describe( 'docker-compose wrapper', () => { + describe( 'proxied methods forward args and return out', () => { + PROXIED_METHODS.forEach( ( method ) => { + it( `${ method } forwards args and resolves with out`, async () => { + mockCompose[ method ].mockReturnValue( resolvesWith( `${ method } output` ) ); + + const result = await dc[ method ]( 'arg1', 'arg2' ); + + expect( mockCompose[ method ] ).toHaveBeenCalledWith( 'arg1', 'arg2' ); + expect( result ).toBe( `${ method } output` ); + } ); + } ); + } ); + + describe( 'proxied methods throw when exitCode is truthy', () => { + PROXIED_METHODS.forEach( ( method ) => { + it( `${ method } throws the err string on non-zero exitCode`, async () => { + mockCompose[ method ].mockReturnValue( rejectsWith( `${ method } failed` ) ); + + await expect( dc[ method ]() ).rejects.toThrow( `${ method } failed` ); + } ); + } ); + } ); + + describe( 'isRunning', () => { + it( 'returns true when compose.port resolves', async () => { + mockCompose.port.mockResolvedValue( {} ); + + const result = await dc.isRunning( '/some/cwd' ); + + expect( result ).toBe( true ); + expect( mockCompose.port ).toHaveBeenCalledWith( 'nginx', 80, { cwd: '/some/cwd' } ); + } ); + + it( 'returns false when compose.port rejects', async () => { + mockCompose.port.mockRejectedValue( new Error( 'not running' ) ); + + const result = await dc.isRunning( '/some/cwd' ); + + expect( result ).toBe( false ); + } ); + } ); +} ); From 25a35f9234b1698bb20c0f5a6960e5984f94026e Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Fri, 5 Jun 2026 12:03:21 +0000 Subject: [PATCH 08/16] test: add environment integration tests (20 tests) Covers start/stop/restart/deleteEnv orchestration and all *All iteration functions. Verifies call ordering (down before upAll on restart), the confirm=false short-circuit in deleteEnv, sudo host removal path, and spinner.warn on sudo error. Co-Authored-By: Claude Sonnet 4.6 --- prd.md | 2 +- progress.txt | 6 + test/integration/environment.test.js | 286 +++++++++++++++++++++++++++ 3 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 test/integration/environment.test.js diff --git a/prd.md b/prd.md index e54d2c69..790d8fb9 100644 --- a/prd.md +++ b/prd.md @@ -81,7 +81,7 @@ Decisions confirmed with the user: **Jest**, **comprehensive** scope, **wire int | ------------------------------------ | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ✅ `create/make-docker-compose.test.js` | `src/commands/create/make-docker-compose.js` | `../../configure` (`config.get('snapshotsPath')`), `os.platform`, `process.env.USER`/`process.getuid` for the Linux branch | The generated compose object across matrix: PHP version → correct `images[...]`; `wordpress.type` `dev` vs other → nginx conf + wp-cli volume; `elasticsearch` on/off → service+volume; `certs` on/off → `CERT_NAME`; **Linux vs non-Linux** branch (build args, ssh/aws/wpsnapshots volumes); the `settings.dockerCompose` filter hook. Snapshot for full object plus targeted assertions — 17 tests (2026-06-05) | | ✅ `utils/docker-compose.test.js` | `src/utils/docker-compose.js` | `docker-compose` npm package | Each proxied method (`down/exec/logs/ps/pullAll/restartAll/run/upAll`) forwards args and returns `out`; throws `err` when `exitCode` is truthy (`interpretComposerResults`); `isRunning` → true when `port` resolves, false when it rejects — 18 tests (2026-06-05) | -| `environment.test.js` | `src/environment.js` | `./configure`, `./database`, `./env-utils`, `./gateway`, `./utils/docker-compose`, `fs-extra`, `inquirer`, `which`, `@vscode/sudo-prompt` | Orchestration & branching: `start` (pull vs no-pull, calls `gateway.startGlobal` then `compose.upAll`), `stop`, `restart` (`isRunning` true→down first), `deleteEnv` (confirm=false short-circuits; confirm=true removes files/certs/db; `manageHosts` true→sudo hosts removal), `startAll/stopAll/restartAll/deleteAll` iterate envs. Assert call order/args, not Docker behavior. | +| ✅ `environment.test.js` | `src/environment.js` | `./configure`, `./database`, `./env-utils`, `./gateway`, `./utils/docker-compose`, `fs-extra`, `inquirer`, `which`, `@vscode/sudo-prompt` | Orchestration & branching: `start` (pull vs no-pull, calls `gateway.startGlobal` then `compose.upAll`), `stop`, `restart` (`isRunning` true→down first), `deleteEnv` (confirm=false short-circuits; confirm=true removes files/certs/db; `manageHosts` true→sudo hosts removal), `startAll/stopAll/restartAll/deleteAll` iterate envs. Assert call order/args, not Docker behavior — 20 tests (2026-06-05) | | `gateway.test.js` | `src/gateway.js` | `./utils/make-docker` (mock docker w/ `getNetwork`/`getVolume`/`createNetwork`/`createVolume`), `./utils/docker-compose`, `./configure`, `fs`, `netcat/client` | `ensureNetworkExists` (creates when `inspect` rejects, skips when it resolves), `removeNetwork`, `ensureCacheExists`/`removeCacheVolume`, `startGlobal` idempotency via the module-level `started` flag, `waitForDB` (fake timers + mock netcat emitting `data`). Use `jest.resetModules()` per test to reset `started`. | | `database.test.js` | `src/database.js` | `mysql` (mock `createConnection` → `{ query(sql, cb), destroy() }`) | `create`/`deleteDatabase`/`assignPrivs` emit the correct SQL, `destroy()` the connection, resolve on success, reject on query error | | `certificates.test.js` | `src/certificates.js` | `child_process` (`execSync`), `fs` (`promises.readFile/writeFile`), `mkcert`, `mkcert-prebuilt`, `./configure` (`getSslCertsDirectory`), `./env-utils` | `getCARoot` (returns trimmed CAROOT), `installCA` (true on success, false on throw), `generate` (reads rootCA files, calls `mkcert.createCert` with `allHosts` incl. wildcards, writes `.crt`/`.key`, returns paths) | diff --git a/progress.txt b/progress.txt index 4dd00c17..ba24e5e3 100644 --- a/progress.txt +++ b/progress.txt @@ -31,6 +31,12 @@ - Mocked docker-compose npm package entirely; all 8 proxied methods tested for arg-forwarding and exitCode error throwing via interpretComposerResults; isRunning tested for resolve→true and reject→false - npm test: 124/124 pass; npm run lint: clean +2026-06-05: environment integration tests +- Created test/integration/environment.test.js: 20 passing tests +- Mocked ./configure (get→manageHosts, getSslCertsDirectory), ./database, ./env-utils (getPathOrError, envSlug, getEnvHosts, getAllEnvironments), ./gateway (startGlobal/stopGlobal/restartGlobal), ./utils/docker-compose, fs-extra, inquirer, which, @vscode/sudo-prompt +- Covers: start (pull vs no-pull, log flag, spinner calls), stop (cwd/log), restart (down-before-upAll when isRunning=true, skip down when false), deleteEnv (confirm=false short-circuits; confirm=true removes files/certs/db; manageHosts=true→sudo.exec; sudo error→spinner.warn), startAll/stopAll/restartAll/deleteAll iteration +- npm test: 144/144 pass; npm run lint: clean + 2026-06-05: create/inquirer unit tests - Created test/unit/create/inquirer.test.js: 21 passing tests for makeInquirer via stub prompt - Covers marshalDomains (single hostname, scalar domain passthrough, Set deduplication, extraHosts merging), marshalWordPress (boolean→{}, title/user/pass/email, type, purify, emptyContent), and top-level field passthrough (php, name, elasticsearch, mediaProxy) diff --git a/test/integration/environment.test.js b/test/integration/environment.test.js new file mode 100644 index 00000000..3a5c6187 --- /dev/null +++ b/test/integration/environment.test.js @@ -0,0 +1,286 @@ +'use strict'; + +const mockCompose = { + down: jest.fn(), + upAll: jest.fn(), + pullAll: jest.fn(), + isRunning: jest.fn(), +}; + +const mockGateway = { + startGlobal: jest.fn(), + stopGlobal: jest.fn(), + restartGlobal: jest.fn(), +}; + +const mockDatabase = { + deleteDatabase: jest.fn(), +}; + +const mockEnvUtils = { + getPathOrError: jest.fn(), + envSlug: jest.fn(), + getEnvHosts: jest.fn(), + getAllEnvironments: jest.fn(), +}; + +const mockConfig = { + get: jest.fn(), + getSslCertsDirectory: jest.fn(), +}; + +const mockFsExtra = { + remove: jest.fn(), +}; + +const mockInquirer = { + prompt: jest.fn(), +}; + +const mockWhich = jest.fn(); +const mockSudo = { exec: jest.fn() }; + +jest.mock( '../../src/configure', () => mockConfig ); +jest.mock( '../../src/database', () => mockDatabase ); +jest.mock( '../../src/env-utils', () => mockEnvUtils ); +jest.mock( '../../src/gateway', () => mockGateway ); +jest.mock( '../../src/utils/docker-compose', () => mockCompose ); +jest.mock( 'fs-extra', () => mockFsExtra ); +jest.mock( 'inquirer', () => mockInquirer ); +jest.mock( 'which', () => mockWhich ); +jest.mock( '@vscode/sudo-prompt', () => mockSudo ); + +const environment = require( '../../src/environment' ); + +function makeSpinner() { + return { + start: jest.fn(), + succeed: jest.fn(), + warn: jest.fn(), + }; +} + +const ENV_NAME = 'mysite-test'; +const ENV_PATH = '/sites/mysite-test'; +const ENV_SLUG = 'mysite-test'; +const ENV_HOSTS = [ 'mysite.test', 'www.mysite.test' ]; +const SSL_DIR = '/home/user/.wplocaldocker/global/ssl-certs'; + +beforeEach( () => { + mockEnvUtils.getPathOrError.mockResolvedValue( ENV_PATH ); + mockEnvUtils.envSlug.mockReturnValue( ENV_SLUG ); + mockEnvUtils.getEnvHosts.mockResolvedValue( ENV_HOSTS ); + mockEnvUtils.getAllEnvironments.mockResolvedValue( [ ENV_NAME ] ); + mockGateway.startGlobal.mockResolvedValue(); + mockGateway.stopGlobal.mockResolvedValue(); + mockGateway.restartGlobal.mockResolvedValue(); + mockCompose.upAll.mockResolvedValue(); + mockCompose.down.mockResolvedValue(); + mockCompose.pullAll.mockResolvedValue(); + mockCompose.isRunning.mockResolvedValue( false ); + mockDatabase.deleteDatabase.mockResolvedValue(); + mockFsExtra.remove.mockResolvedValue(); + mockConfig.getSslCertsDirectory.mockResolvedValue( SSL_DIR ); + mockConfig.get.mockResolvedValue( false ); + mockInquirer.prompt.mockResolvedValue( { confirm: true } ); + mockWhich.mockResolvedValue( '/usr/local/bin/node' ); + mockSudo.exec.mockImplementation( ( cmd, opts, cb ) => cb( null, 'ok' ) ); +} ); + +describe( 'environment', () => { + describe( 'start', () => { + it( 'calls startGlobal and upAll without pulling when pull=false', async () => { + const spinner = makeSpinner(); + await environment.start( ENV_NAME, spinner, false ); + + expect( mockEnvUtils.getPathOrError ).toHaveBeenCalledWith( ENV_NAME, spinner ); + expect( mockGateway.startGlobal ).toHaveBeenCalledWith( spinner, false ); + expect( mockCompose.pullAll ).not.toHaveBeenCalled(); + expect( mockCompose.upAll ).toHaveBeenCalledWith( { cwd: ENV_PATH, log: false } ); + } ); + + it( 'pulls images before upAll when pull=true', async () => { + const spinner = makeSpinner(); + await environment.start( ENV_NAME, spinner, true ); + + expect( mockCompose.pullAll ).toHaveBeenCalledWith( { cwd: ENV_PATH, log: false } ); + expect( mockCompose.upAll ).toHaveBeenCalledWith( { cwd: ENV_PATH, log: false } ); + } ); + + it( 'sets log=true when no spinner is provided', async () => { + await environment.start( ENV_NAME, null, false ); + + expect( mockCompose.upAll ).toHaveBeenCalledWith( { cwd: ENV_PATH, log: true } ); + } ); + + it( 'calls spinner.start and spinner.succeed', async () => { + const spinner = makeSpinner(); + await environment.start( ENV_NAME, spinner, false ); + + expect( spinner.start ).toHaveBeenCalled(); + expect( spinner.succeed ).toHaveBeenCalled(); + } ); + } ); + + describe( 'stop', () => { + it( 'calls compose.down with the env cwd', async () => { + const spinner = makeSpinner(); + await environment.stop( ENV_NAME, spinner ); + + expect( mockEnvUtils.getPathOrError ).toHaveBeenCalledWith( ENV_NAME, spinner ); + expect( mockCompose.down ).toHaveBeenCalledWith( { cwd: ENV_PATH, log: false } ); + } ); + + it( 'sets log=true when no spinner is provided', async () => { + await environment.stop( ENV_NAME, null ); + + expect( mockCompose.down ).toHaveBeenCalledWith( { cwd: ENV_PATH, log: true } ); + } ); + } ); + + describe( 'restart', () => { + it( 'calls compose.down before upAll when isRunning=true', async () => { + mockCompose.isRunning.mockResolvedValue( true ); + const callOrder = []; + mockCompose.down.mockImplementation( () => { callOrder.push( 'down' ); return Promise.resolve(); } ); + mockCompose.upAll.mockImplementation( () => { callOrder.push( 'upAll' ); return Promise.resolve(); } ); + const spinner = makeSpinner(); + + await environment.restart( ENV_NAME, spinner ); + + expect( callOrder ).toEqual( [ 'down', 'upAll' ] ); + } ); + + it( 'skips compose.down when isRunning=false', async () => { + mockCompose.isRunning.mockResolvedValue( false ); + const spinner = makeSpinner(); + await environment.restart( ENV_NAME, spinner ); + + expect( mockCompose.down ).not.toHaveBeenCalled(); + expect( mockCompose.upAll ).toHaveBeenCalled(); + } ); + + it( 'calls startGlobal and then upAll', async () => { + const spinner = makeSpinner(); + await environment.restart( ENV_NAME, spinner ); + + expect( mockGateway.startGlobal ).toHaveBeenCalledWith( spinner ); + expect( mockCompose.upAll ).toHaveBeenCalledWith( { cwd: ENV_PATH, log: false } ); + } ); + } ); + + describe( 'deleteEnv', () => { + it( 'returns early without removing anything when confirm=false', async () => { + mockInquirer.prompt.mockResolvedValue( { confirm: false } ); + const spinner = makeSpinner(); + await environment.deleteEnv( ENV_NAME, spinner ); + + expect( mockFsExtra.remove ).not.toHaveBeenCalled(); + expect( mockDatabase.deleteDatabase ).not.toHaveBeenCalled(); + } ); + + it( 'removes env directory, certificates, and database when confirm=true', async () => { + mockInquirer.prompt.mockResolvedValue( { confirm: true } ); + const spinner = makeSpinner(); + await environment.deleteEnv( ENV_NAME, spinner ); + + expect( mockFsExtra.remove ).toHaveBeenCalledWith( ENV_PATH ); + expect( mockFsExtra.remove ).toHaveBeenCalledWith( `${ SSL_DIR }/${ ENV_SLUG }.crt` ); + expect( mockFsExtra.remove ).toHaveBeenCalledWith( `${ SSL_DIR }/${ ENV_SLUG }.key` ); + expect( mockDatabase.deleteDatabase ).toHaveBeenCalledWith( ENV_SLUG ); + } ); + + it( 'calls compose.down with -v commandOptions', async () => { + mockInquirer.prompt.mockResolvedValue( { confirm: true } ); + await environment.deleteEnv( ENV_NAME, null ); + + expect( mockCompose.down ).toHaveBeenCalledWith( { + cwd: ENV_PATH, + log: true, + commandOptions: [ '-v' ], + } ); + } ); + + it( 'calls getSslCertsDirectory with false to skip dir creation', async () => { + mockInquirer.prompt.mockResolvedValue( { confirm: true } ); + await environment.deleteEnv( ENV_NAME, null ); + + expect( mockConfig.getSslCertsDirectory ).toHaveBeenCalledWith( false ); + } ); + + it( 'calls sudo.exec to remove host entries when manageHosts=true', async () => { + mockConfig.get.mockResolvedValue( true ); + mockInquirer.prompt.mockResolvedValue( { confirm: true } ); + const spinner = makeSpinner(); + await environment.deleteEnv( ENV_NAME, spinner ); + + expect( mockWhich ).toHaveBeenCalledWith( 'node' ); + expect( mockSudo.exec ).toHaveBeenCalled(); + } ); + + it( 'does not call sudo.exec when manageHosts=false', async () => { + mockConfig.get.mockResolvedValue( false ); + mockInquirer.prompt.mockResolvedValue( { confirm: true } ); + await environment.deleteEnv( ENV_NAME, null ); + + expect( mockSudo.exec ).not.toHaveBeenCalled(); + } ); + + it( 'calls spinner.warn when sudo.exec callback receives an error', async () => { + mockConfig.get.mockResolvedValue( true ); + mockInquirer.prompt.mockResolvedValue( { confirm: true } ); + mockSudo.exec.mockImplementation( ( cmd, opts, cb ) => cb( new Error( 'sudo failed' ) ) ); + const spinner = makeSpinner(); + await environment.deleteEnv( ENV_NAME, spinner ); + + expect( spinner.warn ).toHaveBeenCalled(); + } ); + } ); + + describe( 'startAll', () => { + it( 'calls startGlobal once up-front and start for each environment', async () => { + mockEnvUtils.getAllEnvironments.mockResolvedValue( [ 'site-a', 'site-b' ] ); + const spinner = makeSpinner(); + await environment.startAll( spinner, false ); + + expect( mockEnvUtils.getAllEnvironments ).toHaveBeenCalled(); + // startGlobal: 1 from startAll + 1 per start() call = 3 total + expect( mockGateway.startGlobal ).toHaveBeenCalledTimes( 3 ); + expect( mockCompose.upAll ).toHaveBeenCalledTimes( 2 ); + } ); + } ); + + describe( 'stopAll', () => { + it( 'stops each environment then calls stopGlobal', async () => { + mockEnvUtils.getAllEnvironments.mockResolvedValue( [ 'site-a', 'site-b' ] ); + const spinner = makeSpinner(); + await environment.stopAll( spinner ); + + expect( mockCompose.down ).toHaveBeenCalledTimes( 2 ); + expect( mockGateway.stopGlobal ).toHaveBeenCalledWith( spinner ); + } ); + } ); + + describe( 'restartAll', () => { + it( 'restarts each environment then calls restartGlobal', async () => { + mockEnvUtils.getAllEnvironments.mockResolvedValue( [ 'site-a', 'site-b' ] ); + const spinner = makeSpinner(); + await environment.restartAll( spinner ); + + expect( mockCompose.upAll ).toHaveBeenCalledTimes( 2 ); + expect( mockGateway.restartGlobal ).toHaveBeenCalledWith( spinner ); + } ); + } ); + + describe( 'deleteAll', () => { + it( 'calls deleteEnv for each environment', async () => { + mockEnvUtils.getAllEnvironments.mockResolvedValue( [ 'site-a', 'site-b' ] ); + mockInquirer.prompt.mockResolvedValue( { confirm: false } ); + const spinner = makeSpinner(); + await environment.deleteAll( spinner ); + + expect( mockEnvUtils.getAllEnvironments ).toHaveBeenCalled(); + expect( mockInquirer.prompt ).toHaveBeenCalledTimes( 2 ); + } ); + } ); +} ); From b16e289cf01dbc9e7eb66888c4cdf5b8d34052a9 Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Fri, 5 Jun 2026 12:10:13 +0000 Subject: [PATCH 09/16] test: add gateway integration tests (14 tests) Co-Authored-By: Claude Sonnet 4.6 --- prd.md | 2 +- progress.txt | 8 + test/integration/gateway.test.js | 271 +++++++++++++++++++++++++++++++ 3 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 test/integration/gateway.test.js diff --git a/prd.md b/prd.md index 790d8fb9..386777c2 100644 --- a/prd.md +++ b/prd.md @@ -82,7 +82,7 @@ Decisions confirmed with the user: **Jest**, **comprehensive** scope, **wire int | ✅ `create/make-docker-compose.test.js` | `src/commands/create/make-docker-compose.js` | `../../configure` (`config.get('snapshotsPath')`), `os.platform`, `process.env.USER`/`process.getuid` for the Linux branch | The generated compose object across matrix: PHP version → correct `images[...]`; `wordpress.type` `dev` vs other → nginx conf + wp-cli volume; `elasticsearch` on/off → service+volume; `certs` on/off → `CERT_NAME`; **Linux vs non-Linux** branch (build args, ssh/aws/wpsnapshots volumes); the `settings.dockerCompose` filter hook. Snapshot for full object plus targeted assertions — 17 tests (2026-06-05) | | ✅ `utils/docker-compose.test.js` | `src/utils/docker-compose.js` | `docker-compose` npm package | Each proxied method (`down/exec/logs/ps/pullAll/restartAll/run/upAll`) forwards args and returns `out`; throws `err` when `exitCode` is truthy (`interpretComposerResults`); `isRunning` → true when `port` resolves, false when it rejects — 18 tests (2026-06-05) | | ✅ `environment.test.js` | `src/environment.js` | `./configure`, `./database`, `./env-utils`, `./gateway`, `./utils/docker-compose`, `fs-extra`, `inquirer`, `which`, `@vscode/sudo-prompt` | Orchestration & branching: `start` (pull vs no-pull, calls `gateway.startGlobal` then `compose.upAll`), `stop`, `restart` (`isRunning` true→down first), `deleteEnv` (confirm=false short-circuits; confirm=true removes files/certs/db; `manageHosts` true→sudo hosts removal), `startAll/stopAll/restartAll/deleteAll` iterate envs. Assert call order/args, not Docker behavior — 20 tests (2026-06-05) | -| `gateway.test.js` | `src/gateway.js` | `./utils/make-docker` (mock docker w/ `getNetwork`/`getVolume`/`createNetwork`/`createVolume`), `./utils/docker-compose`, `./configure`, `fs`, `netcat/client` | `ensureNetworkExists` (creates when `inspect` rejects, skips when it resolves), `removeNetwork`, `ensureCacheExists`/`removeCacheVolume`, `startGlobal` idempotency via the module-level `started` flag, `waitForDB` (fake timers + mock netcat emitting `data`). Use `jest.resetModules()` per test to reset `started`. | +| ✅ `gateway.test.js` | `src/gateway.js` | `./utils/make-docker` (mock docker w/ `getNetwork`/`getVolume`/`createNetwork`/`createVolume`), `./utils/docker-compose`, `./configure`, `fs`, `netcat/client` | `ensureNetworkExists` (creates when `inspect` rejects, skips when it resolves), `ensureCacheExists`/`removeCacheVolume`, `startGlobal` idempotency via the module-level `started` flag, `waitForDB` via fake timers + mock netcat `connect()` emitting `data` via `setImmediate`, `stopGlobal`/`restartGlobal` error swallowing. Uses `jest.resetModules()` + re-require per test to reset `started` — 14 tests (2026-06-05) | | `database.test.js` | `src/database.js` | `mysql` (mock `createConnection` → `{ query(sql, cb), destroy() }`) | `create`/`deleteDatabase`/`assignPrivs` emit the correct SQL, `destroy()` the connection, resolve on success, reject on query error | | `certificates.test.js` | `src/certificates.js` | `child_process` (`execSync`), `fs` (`promises.readFile/writeFile`), `mkcert`, `mkcert-prebuilt`, `./configure` (`getSslCertsDirectory`), `./env-utils` | `getCARoot` (returns trimmed CAROOT), `installCA` (true on success, false on throw), `generate` (reads rootCA files, calls `mkcert.createCert` with `allHosts` incl. wildcards, writes `.crt`/`.key`, returns paths) | diff --git a/progress.txt b/progress.txt index ba24e5e3..58606b9b 100644 --- a/progress.txt +++ b/progress.txt @@ -41,3 +41,11 @@ - Created test/unit/create/inquirer.test.js: 21 passing tests for makeInquirer via stub prompt - Covers marshalDomains (single hostname, scalar domain passthrough, Set deduplication, extraHosts merging), marshalWordPress (boolean→{}, title/user/pass/email, type, purify, emptyContent), and top-level field passthrough (php, name, elasticsearch, mediaProxy) - npm test: 89/89 pass; npm run lint: clean + +2026-06-05: gateway integration tests +- Created test/integration/gateway.test.js: 14 passing tests +- Mocked ../../src/utils/make-docker (var-hoisted mockDocker swap), ../../src/utils/docker-compose, ../../src/configure, ../../src/env-utils, fs, netcat/client +- Covers: ensureNetworkExists (create when inspect rejects, skip when it resolves, console log branch), ensureCacheExists (create vs skip), removeCacheVolume (remove vs skip), startGlobal (getNetwork+getVolume+upAll in order, idempotent started flag, pullAll before upAll when pull=true), stopGlobal (down+removeNetwork, error swallowing), restartGlobal (ensureNetwork+restartAll, error swallowing) +- waitForDB tested via jest.useFakeTimers()+jest.runAllTimersAsync(); nc mock's connect() emits 'data' via setImmediate so listener is registered first +- jest.resetModules()+re-require per test resets the module-level started flag +- npm test: 158/158 pass; npm run lint: clean diff --git a/test/integration/gateway.test.js b/test/integration/gateway.test.js new file mode 100644 index 00000000..29bc1eb1 --- /dev/null +++ b/test/integration/gateway.test.js @@ -0,0 +1,271 @@ +'use strict'; + +const EventEmitter = require( 'events' ); + +// Tracks nc instances created; tests can assert on them or emit 'data' to resolve waitForDB +let mockNcInstances = []; + +// Each nc instance's connect() fires 'data' after the listener is registered +// (setImmediate ensures the on('data') call happens first) +function mockMakeNc() { + const inst = new EventEmitter(); + inst.address = jest.fn(); + inst.port = jest.fn(); + inst.connect = jest.fn().mockImplementation( () => { + setImmediate( () => inst.emit( 'data' ) ); + } ); + inst.close = jest.fn(); + mockNcInstances.push( inst ); + return inst; +} + +const mockCompose = { + upAll: jest.fn(), + down: jest.fn(), + pullAll: jest.fn(), + restartAll: jest.fn(), +}; + +const mockConfig = { + getConfigDirectory: jest.fn(), +}; + +const mockEnvUtils = { + globalPath: '/home/user/.wplocaldocker/global', + cacheVolume: 'wplocaldockerCache', +}; + +jest.mock( '../../src/utils/docker-compose', () => mockCompose ); +jest.mock( '../../src/configure', () => mockConfig ); +jest.mock( '../../src/env-utils', () => mockEnvUtils ); +jest.mock( 'fs', () => ( { existsSync: jest.fn( () => false ) } ) ); +jest.mock( 'netcat/client', () => jest.fn( () => mockMakeNc() ) ); +// make-docker is mocked via a var so we can swap mockDocker per test +jest.mock( '../../src/utils/make-docker', () => () => mockDocker ); // eslint-disable-line no-undef + +function makeDockerMock() { + const network = { + inspect: jest.fn(), + remove: jest.fn(), + }; + const volume = { + inspect: jest.fn(), + remove: jest.fn(), + }; + return { + _network: network, + _volume: volume, + getNetwork: jest.fn( () => network ), + getVolume: jest.fn( () => volume ), + createNetwork: jest.fn(), + createVolume: jest.fn(), + }; +} + +// var so the jest.mock factory above (hoisted) can reference it +// eslint-disable-next-line no-var +var mockDocker; // eslint-disable-line vars-on-top + +function makeSpinner() { + return { + start: jest.fn(), + succeed: jest.fn(), + warn: jest.fn(), + }; +} + +beforeEach( () => { + mockDocker = makeDockerMock(); + mockNcInstances = []; + mockCompose.upAll.mockResolvedValue(); + mockCompose.down.mockResolvedValue(); + mockCompose.pullAll.mockResolvedValue(); + mockCompose.restartAll.mockResolvedValue(); + mockConfig.getConfigDirectory.mockReturnValue( '/home/user/.wplocaldocker' ); +} ); + +// Gateway has module-level `started` flag; reload per test to reset it. +function loadGateway() { + jest.resetModules(); + jest.mock( '../../src/utils/make-docker', () => () => mockDocker ); + jest.mock( '../../src/utils/docker-compose', () => mockCompose ); + jest.mock( '../../src/configure', () => mockConfig ); + jest.mock( '../../src/env-utils', () => mockEnvUtils ); + jest.mock( 'fs', () => ( { existsSync: jest.fn( () => false ) } ) ); + jest.mock( 'netcat/client', () => jest.fn( () => mockMakeNc() ) ); + return require( '../../src/gateway' ); +} + +describe( 'gateway', () => { + describe( 'ensureNetworkExists', () => { + it( 'does nothing when the network already exists', async () => { + const gateway = loadGateway(); + mockDocker._network.inspect.mockResolvedValue( { Id: 'abc' } ); + + await gateway.ensureNetworkExists( mockDocker, makeSpinner() ); + + expect( mockDocker.createNetwork ).not.toHaveBeenCalled(); + } ); + + it( 'creates the network when inspect rejects', async () => { + const gateway = loadGateway(); + mockDocker._network.inspect.mockRejectedValue( new Error( 'not found' ) ); + mockDocker.createNetwork.mockResolvedValue(); + + await gateway.ensureNetworkExists( mockDocker, makeSpinner() ); + + expect( mockDocker.createNetwork ).toHaveBeenCalledWith( + expect.objectContaining( { Name: 'wplocaldocker' } ) + ); + } ); + + it( 'logs to console when no spinner is provided', async () => { + const gateway = loadGateway(); + mockDocker._network.inspect.mockResolvedValue( { Id: 'abc' } ); + const spy = jest.spyOn( console, 'log' ).mockImplementation( () => {} ); + + await gateway.ensureNetworkExists( mockDocker, null ); + + expect( spy ).toHaveBeenCalled(); + spy.mockRestore(); + } ); + } ); + + describe( 'ensureCacheExists', () => { + it( 'does nothing when the volume already exists', async () => { + const gateway = loadGateway(); + mockDocker._volume.inspect.mockResolvedValue( { Name: mockEnvUtils.cacheVolume } ); + + await gateway.ensureCacheExists( mockDocker, makeSpinner() ); + + expect( mockDocker.createVolume ).not.toHaveBeenCalled(); + } ); + + it( 'creates the volume when inspect rejects', async () => { + const gateway = loadGateway(); + mockDocker._volume.inspect.mockRejectedValue( new Error( 'not found' ) ); + mockDocker.createVolume.mockResolvedValue(); + + await gateway.ensureCacheExists( mockDocker, makeSpinner() ); + + expect( mockDocker.createVolume ).toHaveBeenCalledWith( + expect.objectContaining( { Name: mockEnvUtils.cacheVolume } ) + ); + } ); + } ); + + describe( 'removeCacheVolume', () => { + it( 'removes the volume when it exists', async () => { + const gateway = loadGateway(); + mockDocker._volume.inspect.mockResolvedValue( { Name: mockEnvUtils.cacheVolume } ); + mockDocker._volume.remove.mockResolvedValue(); + + await gateway.removeCacheVolume( mockDocker, makeSpinner() ); + + expect( mockDocker._volume.remove ).toHaveBeenCalled(); + } ); + + it( 'skips removal when the volume does not exist', async () => { + const gateway = loadGateway(); + mockDocker._volume.inspect.mockRejectedValue( new Error( 'not found' ) ); + + await gateway.removeCacheVolume( mockDocker, makeSpinner() ); + + expect( mockDocker._volume.remove ).not.toHaveBeenCalled(); + } ); + } ); + + describe( 'startGlobal', () => { + beforeEach( () => { + jest.useFakeTimers(); + } ); + + afterEach( () => { + jest.useRealTimers(); + } ); + + it( 'calls getNetwork, getVolume and compose.upAll', async () => { + const gateway = loadGateway(); + mockDocker._network.inspect.mockResolvedValue( {} ); + mockDocker._volume.inspect.mockResolvedValue( {} ); + + const p = gateway.startGlobal( makeSpinner(), false ); + await jest.runAllTimersAsync(); + await p; + + expect( mockDocker.getNetwork ).toHaveBeenCalledWith( 'wplocaldocker' ); + expect( mockDocker.getVolume ).toHaveBeenCalledWith( mockEnvUtils.cacheVolume ); + expect( mockCompose.upAll ).toHaveBeenCalled(); + } ); + + it( 'is idempotent — second call is a no-op', async () => { + const gateway = loadGateway(); + mockDocker._network.inspect.mockResolvedValue( {} ); + mockDocker._volume.inspect.mockResolvedValue( {} ); + + const p1 = gateway.startGlobal( makeSpinner(), false ); + await jest.runAllTimersAsync(); + await p1; + + await gateway.startGlobal( makeSpinner(), false ); + + expect( mockCompose.upAll ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'calls pullAll before upAll when pull=true', async () => { + const gateway = loadGateway(); + mockDocker._network.inspect.mockResolvedValue( {} ); + mockDocker._volume.inspect.mockResolvedValue( {} ); + + const callOrder = []; + mockCompose.pullAll.mockImplementation( () => { callOrder.push( 'pullAll' ); return Promise.resolve(); } ); + mockCompose.upAll.mockImplementation( () => { callOrder.push( 'upAll' ); return Promise.resolve(); } ); + + const p = gateway.startGlobal( makeSpinner(), true ); + await jest.runAllTimersAsync(); + await p; + + expect( callOrder ).toEqual( [ 'pullAll', 'upAll' ] ); + } ); + } ); + + describe( 'stopGlobal', () => { + it( 'calls compose.down and removes the network', async () => { + const gateway = loadGateway(); + mockDocker._network.inspect.mockResolvedValue( {} ); + mockDocker._network.remove.mockResolvedValue(); + + await gateway.stopGlobal( makeSpinner() ); + + expect( mockCompose.down ).toHaveBeenCalled(); + expect( mockDocker._network.remove ).toHaveBeenCalled(); + } ); + + it( 'swallows errors and resolves cleanly', async () => { + const gateway = loadGateway(); + mockCompose.down.mockRejectedValue( new Error( 'compose error' ) ); + + await expect( gateway.stopGlobal( makeSpinner() ) ).resolves.toBeUndefined(); + } ); + } ); + + describe( 'restartGlobal', () => { + it( 'calls ensureNetworkExists then compose.restartAll', async () => { + const gateway = loadGateway(); + mockDocker._network.inspect.mockResolvedValue( {} ); + + await gateway.restartGlobal( makeSpinner() ); + + expect( mockDocker.getNetwork ).toHaveBeenCalledWith( 'wplocaldocker' ); + expect( mockCompose.restartAll ).toHaveBeenCalled(); + } ); + + it( 'swallows errors and resolves cleanly', async () => { + const gateway = loadGateway(); + mockDocker._network.inspect.mockResolvedValue( {} ); + mockCompose.restartAll.mockRejectedValue( new Error( 'restart error' ) ); + + await expect( gateway.restartGlobal( makeSpinner() ) ).resolves.toBeUndefined(); + } ); + } ); +} ); From 3f3ce248486c05406d14a8d59a5b6442b07df8c3 Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Fri, 5 Jun 2026 12:16:31 +0000 Subject: [PATCH 10/16] test: add database integration tests (12 tests) Co-Authored-By: Claude Sonnet 4.6 --- prd.md | 2 +- progress.txt | 6 ++ test/integration/database.test.js | 127 ++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 test/integration/database.test.js diff --git a/prd.md b/prd.md index 386777c2..f4755d06 100644 --- a/prd.md +++ b/prd.md @@ -83,7 +83,7 @@ Decisions confirmed with the user: **Jest**, **comprehensive** scope, **wire int | ✅ `utils/docker-compose.test.js` | `src/utils/docker-compose.js` | `docker-compose` npm package | Each proxied method (`down/exec/logs/ps/pullAll/restartAll/run/upAll`) forwards args and returns `out`; throws `err` when `exitCode` is truthy (`interpretComposerResults`); `isRunning` → true when `port` resolves, false when it rejects — 18 tests (2026-06-05) | | ✅ `environment.test.js` | `src/environment.js` | `./configure`, `./database`, `./env-utils`, `./gateway`, `./utils/docker-compose`, `fs-extra`, `inquirer`, `which`, `@vscode/sudo-prompt` | Orchestration & branching: `start` (pull vs no-pull, calls `gateway.startGlobal` then `compose.upAll`), `stop`, `restart` (`isRunning` true→down first), `deleteEnv` (confirm=false short-circuits; confirm=true removes files/certs/db; `manageHosts` true→sudo hosts removal), `startAll/stopAll/restartAll/deleteAll` iterate envs. Assert call order/args, not Docker behavior — 20 tests (2026-06-05) | | ✅ `gateway.test.js` | `src/gateway.js` | `./utils/make-docker` (mock docker w/ `getNetwork`/`getVolume`/`createNetwork`/`createVolume`), `./utils/docker-compose`, `./configure`, `fs`, `netcat/client` | `ensureNetworkExists` (creates when `inspect` rejects, skips when it resolves), `ensureCacheExists`/`removeCacheVolume`, `startGlobal` idempotency via the module-level `started` flag, `waitForDB` via fake timers + mock netcat `connect()` emitting `data` via `setImmediate`, `stopGlobal`/`restartGlobal` error swallowing. Uses `jest.resetModules()` + re-require per test to reset `started` — 14 tests (2026-06-05) | -| `database.test.js` | `src/database.js` | `mysql` (mock `createConnection` → `{ query(sql, cb), destroy() }`) | `create`/`deleteDatabase`/`assignPrivs` emit the correct SQL, `destroy()` the connection, resolve on success, reject on query error | +| ✅ `database.test.js` | `src/database.js` | `mysql` (mock `createConnection` → `{ query(sql, cb), destroy() }`) | `create`/`deleteDatabase`/`assignPrivs` emit the correct SQL, `destroy()` the connection, resolve on success, reject on query error — 12 tests (2026-06-05) | | `certificates.test.js` | `src/certificates.js` | `child_process` (`execSync`), `fs` (`promises.readFile/writeFile`), `mkcert`, `mkcert-prebuilt`, `./configure` (`getSslCertsDirectory`), `./env-utils` | `getCARoot` (returns trimmed CAROOT), `installCA` (true on success, false on throw), `generate` (reads rootCA files, calls `mkcert.createCert` with `allHosts` incl. wildcards, writes `.crt`/`.key`, returns paths) | ### `test/helpers/` (shared test utilities, lint-clean) diff --git a/progress.txt b/progress.txt index 58606b9b..fb185c55 100644 --- a/progress.txt +++ b/progress.txt @@ -42,6 +42,12 @@ - Covers marshalDomains (single hostname, scalar domain passthrough, Set deduplication, extraHosts merging), marshalWordPress (boolean→{}, title/user/pass/email, type, purify, emptyContent), and top-level field passthrough (php, name, elasticsearch, mediaProxy) - npm test: 89/89 pass; npm run lint: clean +2026-06-05: database integration tests +- Created test/integration/database.test.js: 12 passing tests +- Mocked mysql module entirely (createConnection returns { query, destroy } jest.fn()) +- Covers: create (correct SQL, destroy called, destroy on error, rejects on error), deleteDatabase (same 4), assignPrivs (same 4) +- npm test: 170/170 pass; npm run lint: clean + 2026-06-05: gateway integration tests - Created test/integration/gateway.test.js: 14 passing tests - Mocked ../../src/utils/make-docker (var-hoisted mockDocker swap), ../../src/utils/docker-compose, ../../src/configure, ../../src/env-utils, fs, netcat/client diff --git a/test/integration/database.test.js b/test/integration/database.test.js new file mode 100644 index 00000000..09bb1cfe --- /dev/null +++ b/test/integration/database.test.js @@ -0,0 +1,127 @@ +'use strict'; + +let mockQuery; +let mockDestroy; +let mockConnection; + +jest.mock( 'mysql', () => ( { + createConnection: jest.fn( () => mockConnection ), +} ) ); + +const mysql = require( 'mysql' ); +const { create, deleteDatabase, assignPrivs } = require( '../../src/database' ); + +beforeEach( () => { + mockDestroy = jest.fn(); + mockQuery = jest.fn(); + mockConnection = { query: mockQuery, destroy: mockDestroy }; + mysql.createConnection.mockReturnValue( mockConnection ); +} ); + +describe( 'create', () => { + it( 'issues CREATE DATABASE IF NOT EXISTS with correct dbname', async () => { + mockQuery.mockImplementation( ( sql, cb ) => cb( null ) ); + + await create( 'mydb' ); + + expect( mockQuery ).toHaveBeenCalledWith( + 'CREATE DATABASE IF NOT EXISTS `mydb`;', + expect.any( Function ) + ); + } ); + + it( 'calls destroy on success', async () => { + mockQuery.mockImplementation( ( sql, cb ) => cb( null ) ); + + await create( 'mydb' ); + + expect( mockDestroy ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'calls destroy before rejecting on error', async () => { + const calls = []; + mockDestroy.mockImplementation( () => calls.push( 'destroy' ) ); + mockQuery.mockImplementation( ( sql, cb ) => cb( new Error( 'query failed' ) ) ); + + await expect( create( 'mydb' ) ).rejects.toThrow(); + expect( calls ).toContain( 'destroy' ); + } ); + + it( 'rejects with an Error when query errors', async () => { + mockQuery.mockImplementation( ( sql, cb ) => cb( new Error( 'query failed' ) ) ); + + await expect( create( 'mydb' ) ).rejects.toBeInstanceOf( Error ); + } ); +} ); + +describe( 'deleteDatabase', () => { + it( 'issues DROP DATABASE IF EXISTS with correct dbname', async () => { + mockQuery.mockImplementation( ( sql, cb ) => cb( null ) ); + + await deleteDatabase( 'mydb' ); + + expect( mockQuery ).toHaveBeenCalledWith( + 'DROP DATABASE IF EXISTS `mydb`;', + expect.any( Function ) + ); + } ); + + it( 'calls destroy on success', async () => { + mockQuery.mockImplementation( ( sql, cb ) => cb( null ) ); + + await deleteDatabase( 'mydb' ); + + expect( mockDestroy ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'calls destroy before rejecting on error', async () => { + const calls = []; + mockDestroy.mockImplementation( () => calls.push( 'destroy' ) ); + mockQuery.mockImplementation( ( sql, cb ) => cb( new Error( 'drop failed' ) ) ); + + await expect( deleteDatabase( 'mydb' ) ).rejects.toThrow(); + expect( calls ).toContain( 'destroy' ); + } ); + + it( 'rejects with an Error when query errors', async () => { + mockQuery.mockImplementation( ( sql, cb ) => cb( new Error( 'drop failed' ) ) ); + + await expect( deleteDatabase( 'mydb' ) ).rejects.toBeInstanceOf( Error ); + } ); +} ); + +describe( 'assignPrivs', () => { + it( 'issues GRANT ALL PRIVILEGES with correct dbname', async () => { + mockQuery.mockImplementation( ( sql, cb ) => cb( null ) ); + + await assignPrivs( 'mydb' ); + + expect( mockQuery ).toHaveBeenCalledWith( + 'GRANT ALL PRIVILEGES ON `mydb`.* TO \'wordpress\'@\'%\' IDENTIFIED BY \'password\';', + expect.any( Function ) + ); + } ); + + it( 'calls destroy on success', async () => { + mockQuery.mockImplementation( ( sql, cb ) => cb( null ) ); + + await assignPrivs( 'mydb' ); + + expect( mockDestroy ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'calls destroy before rejecting on error', async () => { + const calls = []; + mockDestroy.mockImplementation( () => calls.push( 'destroy' ) ); + mockQuery.mockImplementation( ( sql, cb ) => cb( new Error( 'grant failed' ) ) ); + + await expect( assignPrivs( 'mydb' ) ).rejects.toThrow(); + expect( calls ).toContain( 'destroy' ); + } ); + + it( 'rejects with an Error when query errors', async () => { + mockQuery.mockImplementation( ( sql, cb ) => cb( new Error( 'grant failed' ) ) ); + + await expect( assignPrivs( 'mydb' ) ).rejects.toBeInstanceOf( Error ); + } ); +} ); From a76129809c22123c908edd31e0c5b0eece8c3d15 Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Fri, 5 Jun 2026 12:18:36 +0000 Subject: [PATCH 11/16] test: add certificates integration tests (17 tests) Co-Authored-By: Claude Sonnet 4.6 --- prd.md | 2 +- progress.txt | 6 + test/integration/certificates.test.js | 240 ++++++++++++++++++++++++++ 3 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 test/integration/certificates.test.js diff --git a/prd.md b/prd.md index f4755d06..b6955c64 100644 --- a/prd.md +++ b/prd.md @@ -84,7 +84,7 @@ Decisions confirmed with the user: **Jest**, **comprehensive** scope, **wire int | ✅ `environment.test.js` | `src/environment.js` | `./configure`, `./database`, `./env-utils`, `./gateway`, `./utils/docker-compose`, `fs-extra`, `inquirer`, `which`, `@vscode/sudo-prompt` | Orchestration & branching: `start` (pull vs no-pull, calls `gateway.startGlobal` then `compose.upAll`), `stop`, `restart` (`isRunning` true→down first), `deleteEnv` (confirm=false short-circuits; confirm=true removes files/certs/db; `manageHosts` true→sudo hosts removal), `startAll/stopAll/restartAll/deleteAll` iterate envs. Assert call order/args, not Docker behavior — 20 tests (2026-06-05) | | ✅ `gateway.test.js` | `src/gateway.js` | `./utils/make-docker` (mock docker w/ `getNetwork`/`getVolume`/`createNetwork`/`createVolume`), `./utils/docker-compose`, `./configure`, `fs`, `netcat/client` | `ensureNetworkExists` (creates when `inspect` rejects, skips when it resolves), `ensureCacheExists`/`removeCacheVolume`, `startGlobal` idempotency via the module-level `started` flag, `waitForDB` via fake timers + mock netcat `connect()` emitting `data` via `setImmediate`, `stopGlobal`/`restartGlobal` error swallowing. Uses `jest.resetModules()` + re-require per test to reset `started` — 14 tests (2026-06-05) | | ✅ `database.test.js` | `src/database.js` | `mysql` (mock `createConnection` → `{ query(sql, cb), destroy() }`) | `create`/`deleteDatabase`/`assignPrivs` emit the correct SQL, `destroy()` the connection, resolve on success, reject on query error — 12 tests (2026-06-05) | -| `certificates.test.js` | `src/certificates.js` | `child_process` (`execSync`), `fs` (`promises.readFile/writeFile`), `mkcert`, `mkcert-prebuilt`, `./configure` (`getSslCertsDirectory`), `./env-utils` | `getCARoot` (returns trimmed CAROOT), `installCA` (true on success, false on throw), `generate` (reads rootCA files, calls `mkcert.createCert` with `allHosts` incl. wildcards, writes `.crt`/`.key`, returns paths) | +| ✅ `certificates.test.js` | `src/certificates.js` | `child_process` (`execSync`), `fs` (`promises.readFile/writeFile`), `mkcert`, `mkcert-prebuilt`, `./configure` (`getSslCertsDirectory`), `./env-utils` | `getCARoot` (returns trimmed CAROOT), `installCA` (true on success, false on throw, verbose stdio/logging branches), `generate` (reads rootCA files, calls `mkcert.createCert` with `allHosts` incl. wildcards, writes `.crt`/`.key`, returns paths) — 17 tests (2026-06-05) | ### `test/helpers/` (shared test utilities, lint-clean) diff --git a/progress.txt b/progress.txt index fb185c55..1082ebaf 100644 --- a/progress.txt +++ b/progress.txt @@ -48,6 +48,12 @@ - Covers: create (correct SQL, destroy called, destroy on error, rejects on error), deleteDatabase (same 4), assignPrivs (same 4) - npm test: 170/170 pass; npm run lint: clean +2026-06-05: certificates integration tests +- Created test/integration/certificates.test.js: 17 passing tests +- Mocked child_process (execSync), fs (promises.readFile/writeFile), mkcert (createCert), mkcert-prebuilt (returns path string), ./env-utils (envSlug), ./configure (getSslCertsDirectory) +- Covers: getCARoot (correct command args, returns trimmed path), installCA (stdio:ignore vs inherit, returns true on success, returns false on throw, logs error when verbose=true, silent when verbose=false), generate (envSlug called, reads rootCA-key.pem and rootCA.pem, createCert called with allHosts including wildcards, writes .crt and .key files, returns cert/key paths) +- npm test: 187/187 pass; npm run lint: clean + 2026-06-05: gateway integration tests - Created test/integration/gateway.test.js: 14 passing tests - Mocked ../../src/utils/make-docker (var-hoisted mockDocker swap), ../../src/utils/docker-compose, ../../src/configure, ../../src/env-utils, fs, netcat/client diff --git a/test/integration/certificates.test.js b/test/integration/certificates.test.js new file mode 100644 index 00000000..737b76a9 --- /dev/null +++ b/test/integration/certificates.test.js @@ -0,0 +1,240 @@ +'use strict'; + +let mockExecSync; + +jest.mock( 'child_process', () => ( { + execSync: ( ...args ) => mockExecSync( ...args ), +} ) ); + +const mockReadFile = jest.fn(); +const mockWriteFile = jest.fn(); + +jest.mock( 'fs', () => ( { + promises: { + readFile: ( ...args ) => mockReadFile( ...args ), + writeFile: ( ...args ) => mockWriteFile( ...args ), + }, +} ) ); + +const mockCreateCert = jest.fn(); + +jest.mock( 'mkcert', () => ( { + createCert: ( ...args ) => mockCreateCert( ...args ), +} ) ); + +jest.mock( 'mkcert-prebuilt', () => '/usr/local/bin/mkcert' ); + +const mockEnvSlug = jest.fn(); + +jest.mock( '../../src/env-utils', () => ( { + envSlug: ( ...args ) => mockEnvSlug( ...args ), +} ) ); + +const mockGetSslCertsDirectory = jest.fn(); + +jest.mock( '../../src/configure', () => ( { + getSslCertsDirectory: ( ...args ) => mockGetSslCertsDirectory( ...args ), +} ) ); + +const { getCARoot, installCA, generate } = require( '../../src/certificates' ); + +beforeEach( () => { + mockExecSync = jest.fn(); +} ); + +describe( 'getCARoot', () => { + it( 'calls mkcert-prebuilt with -CAROOT and encoding utf-8', () => { + mockExecSync.mockReturnValue( '/home/user/.local/share/mkcert\n' ); + + getCARoot(); + + expect( mockExecSync ).toHaveBeenCalledWith( + '"/usr/local/bin/mkcert" -CAROOT', + { encoding: 'utf-8' } + ); + } ); + + it( 'returns the trimmed CAROOT path', () => { + mockExecSync.mockReturnValue( ' /home/user/.local/share/mkcert\n ' ); + + const result = getCARoot(); + + expect( result ).toBe( '/home/user/.local/share/mkcert' ); + } ); +} ); + +describe( 'installCA', () => { + it( 'calls mkcert-prebuilt with -install', () => { + mockExecSync.mockReturnValue( undefined ); + + installCA(); + + expect( mockExecSync ).toHaveBeenCalledWith( + '"/usr/local/bin/mkcert" -install', + expect.objectContaining( {} ) + ); + } ); + + it( 'uses stdio ignore by default', () => { + mockExecSync.mockReturnValue( undefined ); + + installCA(); + + expect( mockExecSync ).toHaveBeenCalledWith( + expect.any( String ), + { stdio: 'ignore' } + ); + } ); + + it( 'uses stdio inherit when verbose=true', () => { + mockExecSync.mockReturnValue( undefined ); + + installCA( true ); + + expect( mockExecSync ).toHaveBeenCalledWith( + expect.any( String ), + { stdio: 'inherit' } + ); + } ); + + it( 'returns true on success', () => { + mockExecSync.mockReturnValue( undefined ); + + const result = installCA(); + + expect( result ).toBe( true ); + } ); + + it( 'returns false when execSync throws', () => { + mockExecSync.mockImplementation( () => { + throw new Error( 'mkcert not found' ); + } ); + + const result = installCA(); + + expect( result ).toBe( false ); + } ); + + it( 'returns false without logging when verbose=false and execSync throws', () => { + mockExecSync.mockImplementation( () => { + throw new Error( 'mkcert not found' ); + } ); + const consoleSpy = jest.spyOn( console, 'error' ).mockImplementation( () => {} ); + + installCA( false ); + + expect( consoleSpy ).not.toHaveBeenCalled(); + consoleSpy.mockRestore(); + } ); + + it( 'logs the error when verbose=true and execSync throws', () => { + const err = new Error( 'mkcert not found' ); + mockExecSync.mockImplementation( () => { + throw err; + } ); + const consoleSpy = jest.spyOn( console, 'error' ).mockImplementation( () => {} ); + + installCA( true ); + + expect( consoleSpy ).toHaveBeenCalledWith( err ); + consoleSpy.mockRestore(); + } ); +} ); + +describe( 'generate', () => { + const caRoot = '/home/user/.local/share/mkcert'; + + beforeEach( () => { + mockExecSync.mockReturnValue( `${ caRoot }\n` ); + mockEnvSlug.mockReturnValue( 'my-env' ); + mockGetSslCertsDirectory.mockResolvedValue( '/etc/ssl/certs/wp' ); + mockReadFile.mockImplementation( ( filePath ) => { + if ( filePath.endsWith( 'rootCA-key.pem' ) ) { + return Promise.resolve( '---KEY---' ); + } + return Promise.resolve( '---CERT---' ); + } ); + mockCreateCert.mockResolvedValue( { cert: 'CERT_CONTENT', key: 'KEY_CONTENT' } ); + mockWriteFile.mockResolvedValue( undefined ); + } ); + + it( 'slugifies the envName via envSlug', async () => { + await generate( 'My Env', [ 'example.com' ] ); + + expect( mockEnvSlug ).toHaveBeenCalledWith( 'My Env' ); + } ); + + it( 'reads rootCA-key.pem from the CAROOT directory', async () => { + await generate( 'myenv', [ 'example.com' ] ); + + expect( mockReadFile ).toHaveBeenCalledWith( + `${ caRoot }/rootCA-key.pem`, + { encoding: 'utf-8' } + ); + } ); + + it( 'reads rootCA.pem from the CAROOT directory', async () => { + await generate( 'myenv', [ 'example.com' ] ); + + expect( mockReadFile ).toHaveBeenCalledWith( + `${ caRoot }/rootCA.pem`, + { encoding: 'utf-8' } + ); + } ); + + it( 'calls mkcert.createCert with hosts and their wildcards', async () => { + await generate( 'myenv', [ 'example.com', 'test.local' ] ); + + expect( mockCreateCert ).toHaveBeenCalledWith( { + caCert: '---CERT---', + caKey: '---KEY---', + domains: [ 'example.com', 'test.local', '*.example.com', '*.test.local' ], + validityDays: 365, + } ); + } ); + + it( 'calls mkcert.createCert with caCert and caKey from the read files', async () => { + mockReadFile.mockImplementation( ( filePath ) => { + if ( filePath.endsWith( 'rootCA-key.pem' ) ) { + return Promise.resolve( 'MY_CA_KEY' ); + } + return Promise.resolve( 'MY_CA_CERT' ); + } ); + + await generate( 'myenv', [ 'example.com' ] ); + + expect( mockCreateCert ).toHaveBeenCalledWith( + expect.objectContaining( { + caCert: 'MY_CA_CERT', + caKey: 'MY_CA_KEY', + } ) + ); + } ); + + it( 'writes the cert file to sslDir/.crt', async () => { + await generate( 'myenv', [ 'example.com' ] ); + + expect( mockWriteFile ).toHaveBeenCalledWith( + '/etc/ssl/certs/wp/my-env.crt', + 'CERT_CONTENT' + ); + } ); + + it( 'writes the key file to sslDir/.key', async () => { + await generate( 'myenv', [ 'example.com' ] ); + + expect( mockWriteFile ).toHaveBeenCalledWith( + '/etc/ssl/certs/wp/my-env.key', + 'KEY_CONTENT' + ); + } ); + + it( 'returns the cert and key file paths', async () => { + const result = await generate( 'myenv', [ 'example.com' ] ); + + expect( result ).toEqual( { + cert: '/etc/ssl/certs/wp/my-env.crt', + key: '/etc/ssl/certs/wp/my-env.key', + } ); + } ); +} ); From e99c647156994fe64d5bdc1c650776756ddfab66 Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Fri, 5 Jun 2026 15:17:30 -0300 Subject: [PATCH 12/16] chore: add pre-commit hook (lint, format, unit tests) via husky v9 Run lint-staged (eslint --fix) for lint+format and the unit test suite on every commit. Integration tests are excluded to keep commits fast. Migrate husky from v4 to v9 using the .husky/ directory and core.hooksPath. Co-Authored-By: Claude Opus 4.8 --- .husky/pre-commit | 1 + package-lock.json | 241 +++------------------------------------------- package.json | 12 +-- 3 files changed, 18 insertions(+), 236 deletions(-) create mode 100755 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..cab88561 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged && npm run test:unit diff --git a/package-lock.json b/package-lock.json index 6d39e34a..889695ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "@10up/eslint-config": "^1.0.9", "babel-eslint": "^10.0.3", "eslint": "^7.32.0", - "husky": "^4.3.8", + "husky": "^9.1.7", "jest": "^29.7.0", "lint-staged": "^10.5.4" }, @@ -1948,12 +1948,6 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, - "node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, "node_modules/cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -2102,12 +2096,6 @@ "node": ">= 6" } }, - "node_modules/compare-versions": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", - "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", - "dev": true - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2808,37 +2796,6 @@ "inspect-with-kind": "^1.0.4" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-versions": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz", - "integrity": "sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==", - "dev": true, - "dependencies": { - "semver-regex": "^3.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -3147,33 +3104,19 @@ } }, "node_modules/husky": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz", - "integrity": "sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==", + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, - "hasInstallScript": true, - "dependencies": { - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "compare-versions": "^3.6.0", - "cosmiconfig": "^7.0.0", - "find-versions": "^4.0.0", - "opencollective-postinstall": "^2.0.2", - "pkg-dir": "^5.0.0", - "please-upgrade-node": "^3.2.0", - "slash": "^3.0.0", - "which-pm-runs": "^1.0.0" - }, + "license": "MIT", "bin": { - "husky-run": "bin/run.js", - "husky-upgrade": "lib/upgrader/bin.js" + "husky": "bin.js" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/husky" + "url": "https://github.com/sponsors/typicode" } }, "node_modules/iconv-lite": { @@ -4512,21 +4455,6 @@ "resolve-from-npm": "^3.1.0" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -4995,15 +4923,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/opencollective-postinstall": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", - "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", - "dev": true, - "bin": { - "opencollective-postinstall": "index.js" - } - }, "node_modules/optional": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/optional/-/optional-0.1.4.tgz", @@ -5071,21 +4990,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -5214,18 +5118,6 @@ "node": ">= 6" } }, - "node_modules/pkg-dir": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", - "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", - "dev": true, - "dependencies": { - "find-up": "^5.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/platform-name": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/platform-name/-/platform-name-1.0.0.tgz", @@ -5645,18 +5537,6 @@ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "dev": true }, - "node_modules/semver-regex": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz", - "integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6343,15 +6223,6 @@ "node": ">= 8" } }, - "node_modules/which-pm-runs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", - "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", @@ -7917,12 +7788,6 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, "cjs-module-lexer": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", @@ -8028,12 +7893,6 @@ "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", "dev": true }, - "compare-versions": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", - "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", - "dev": true - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -8570,25 +8429,6 @@ "inspect-with-kind": "^1.0.4" } }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "find-versions": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz", - "integrity": "sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==", - "dev": true, - "requires": { - "semver-regex": "^3.1.2" - } - }, "flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -8808,22 +8648,10 @@ "dev": true }, "husky": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz", - "integrity": "sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "compare-versions": "^3.6.0", - "cosmiconfig": "^7.0.0", - "find-versions": "^4.0.0", - "opencollective-postinstall": "^2.0.2", - "pkg-dir": "^5.0.0", - "please-upgrade-node": "^3.2.0", - "slash": "^3.0.0", - "which-pm-runs": "^1.0.0" - } + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true }, "iconv-lite": { "version": "0.4.24", @@ -9824,15 +9652,6 @@ "resolve-from-npm": "^3.1.0" } }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -10214,12 +10033,6 @@ "mimic-fn": "^2.1.0" } }, - "opencollective-postinstall": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", - "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", - "dev": true - }, "optional": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/optional/-/optional-0.1.4.tgz", @@ -10269,15 +10082,6 @@ "yocto-queue": "^0.1.0" } }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, "p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -10367,15 +10171,6 @@ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true }, - "pkg-dir": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", - "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", - "dev": true, - "requires": { - "find-up": "^5.0.0" - } - }, "platform-name": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/platform-name/-/platform-name-1.0.0.tgz", @@ -10697,12 +10492,6 @@ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", "dev": true }, - "semver-regex": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz", - "integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==", - "dev": true - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11213,12 +11002,6 @@ "isexe": "^2.0.0" } }, - "which-pm-runs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", - "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", - "dev": true - }, "widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", diff --git a/package.json b/package.json index 42e0bda1..643613ba 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,12 @@ "postinstall": "npm run wpdocker postinstall --silent", "wpdocker": "./index.js", "wpdocker-hosts": "./hosts.js", + "prepare": "husky", "lint": "eslint . --ext .js --ignore-pattern node_modules", "format": "npm run lint --silent -- --fix", "test": "jest", + "test:unit": "jest test/unit", + "test:integration": "jest test/integration", "test:watch": "jest --watch", "test:coverage": "jest --coverage" }, @@ -86,16 +89,11 @@ "@10up/eslint-config": "^1.0.9", "babel-eslint": "^10.0.3", "eslint": "^7.32.0", - "husky": "^4.3.8", + "husky": "^9.1.7", "jest": "^29.7.0", "lint-staged": "^10.5.4" }, "lint-staged": { - "*.js": "eslint" - }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } + "*.js": "eslint --fix" } } From 0991b1c085222f423bcb747a37d6a63d7c7bd939 Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Sun, 7 Jun 2026 08:28:36 -0300 Subject: [PATCH 13/16] style: adopt Prettier for code formatting Add prettier and eslint-config-prettier, with .prettierrc.json (tabs, single quotes, 100 print width; 2-space override for YAML) and .prettierignore. Hand formatting to Prettier and keep ESLint for code quality only (extend "prettier" to disable conflicting rules). Switch the format script to "prettier --write", add a "format:check" script, and run both eslint --fix and prettier --write via lint-staged. Reformat the existing codebase accordingly. Co-Authored-By: Claude Opus 4.8 --- .eslintrc.json | 36 +- .github/ISSUE_TEMPLATE/1-bug-report.yml | 122 +++--- .github/ISSUE_TEMPLATE/2-enhancement.yml | 78 ++-- .github/ISSUE_TEMPLATE/3-help.yml | 58 +-- .github/ISSUE_TEMPLATE/config.yml | 6 +- .github/workflows/close-stale-issues.yml | 1 - .prettierignore | 20 + .prettierrc.json | 20 + global/docker-compose.yml | 2 +- hosts.js | 72 ++-- index.js | 30 +- jest.config.js | 4 +- jsconfig.json | 7 +- package-lock.json | 46 ++- package.json | 13 +- src/certificates.js | 52 +-- src/command-utils.js | 34 +- src/commands/cache.js | 4 +- src/commands/cache/clear.js | 22 +- src/commands/cert.js | 4 +- src/commands/cert/generate.js | 111 +++--- src/commands/cert/install.js | 6 +- src/commands/completion.js | 50 +-- src/commands/configure.js | 52 ++- src/commands/create.js | 131 +++---- src/commands/create/copy-configs.js | 40 +- src/commands/create/create-database.js | 22 +- src/commands/create/inquirer.js | 140 ++++--- src/commands/create/install-wordpress.js | 173 +++++---- src/commands/create/make-cert.js | 38 +- src/commands/create/make-docker-compose.js | 122 +++--- src/commands/create/make-fs.js | 40 +- src/commands/create/save-json-file.js | 10 +- src/commands/create/save-yaml-file.js | 25 +- src/commands/create/update-hosts.js | 42 ++- src/commands/delete.js | 30 +- src/commands/image.js | 4 +- src/commands/image/update.js | 168 +++++---- src/commands/list.js | 50 +-- src/commands/logs.js | 89 ++--- src/commands/migrate.js | 181 ++++----- src/commands/postinstall.js | 50 +-- src/commands/restart.js | 28 +- src/commands/shell.js | 60 +-- src/commands/start.js | 68 ++-- src/commands/stop.js | 28 +- src/commands/upgrade.js | 220 ++++++----- src/commands/wp.js | 62 +-- src/commands/wpsnapshots.js | 72 ++-- src/config/elasticsearch/elasticsearch.yml | 8 +- src/configure.js | 145 ++++--- src/database.js | 59 +-- src/env-utils.js | 184 ++++----- src/environment.js | 292 +++++++------- src/gateway.js | 222 +++++------ src/helpers.js | 12 +- src/prompt-validators.js | 34 +- src/utils/display-error.js | 20 +- src/utils/docker-compose.js | 39 +- src/utils/eol-php-versions.js | 8 +- src/utils/make-boxen.js | 17 +- src/utils/make-command.js | 26 +- src/utils/make-docker.js | 8 +- src/utils/make-link.js | 22 +- src/utils/make-markdown.js | 97 ++--- src/utils/make-spinner.js | 8 +- src/utils/make-table.js | 28 +- src/utils/yaml.js | 22 +- test/integration/certificates.test.js | 281 +++++++------- .../create/make-docker-compose.test.js | 257 +++++++------ test/integration/database.test.js | 156 ++++---- test/integration/environment.test.js | 334 ++++++++-------- test/integration/gateway.test.js | 254 +++++++------ test/integration/utils/docker-compose.test.js | 80 ++-- test/unit/configure.test.js | 114 +++--- test/unit/create/inquirer.test.js | 356 +++++++++--------- test/unit/env-utils.test.js | 114 +++--- test/unit/helpers.test.js | 142 +++---- test/unit/prompt-validators.test.js | 239 ++++++------ 79 files changed, 3238 insertions(+), 3083 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc.json diff --git a/.eslintrc.json b/.eslintrc.json index 6ce2da45..2d00b8a0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,31 +1,15 @@ { - "extends": "@10up/eslint-config", + "extends": ["@10up/eslint-config", "prettier"], "env": { "es6": true, "node": true, "jest": true }, "rules": { - "array-bracket-spacing": [ - "error", - "always" - ], "camelcase": [ "error", { - "allow": [ - "depends_on", - "mem_limit", - "mem_reservation", - "cap_add" - ] - } - ], - "indent": [ - "error", - "tab", - { - "SwitchCase": 1 + "allow": ["depends_on", "mem_limit", "mem_reservation", "cap_add"] } ], "no-constant-condition": [ @@ -41,11 +25,6 @@ "allowEmptyCatch": true } ], - "no-trailing-spaces": "error", - "object-curly-spacing": [ - "error", - "always" - ], "prefer-destructuring": [ "error", { @@ -54,13 +33,6 @@ } ], "require-jsdoc": "off", - "template-curly-spacing": [ - "error", - "always" - ], - "yoda": [ - "error", - "never" - ] + "yoda": ["error", "never"] } -} \ No newline at end of file +} diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.yml b/.github/ISSUE_TEMPLATE/1-bug-report.yml index 083fe146..3959d637 100644 --- a/.github/ISSUE_TEMPLATE/1-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/1-bug-report.yml @@ -1,69 +1,69 @@ name: "\U0001F41B Bug report" -description: "Report a bug with this project." -labels: "bug" +description: 'Report a bug with this project.' +labels: 'bug' body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill out this bug report! Please fill in as much of the template below as you can. + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! Please fill in as much of the template below as you can. - - type: checkboxes - id: faqs - attributes: - label: Troubleshooting - description: By submitting this issue, you agree that our [troubleshooting tips](https://github.com/Rahmon/wpdocker#i-am-having-issues-with-wp-local-docker-what-are-the-best-troubleshooting-techniques) have not helped resolve the issue. - options: - - label: I have attempted to troubleshoot this already - required: true - - - type: textarea - attributes: - label: Describe the bug - description: Please write a clear and concise description of the bug, including what you expect to happen and what is currently happening. - placeholder: | - Feature '...' is not working properly. I expect '...' to happen, but '...' happens instead. - validations: + - type: checkboxes + id: faqs + attributes: + label: Troubleshooting + description: By submitting this issue, you agree that our [troubleshooting tips](https://github.com/Rahmon/wpdocker#i-am-having-issues-with-wp-local-docker-what-are-the-best-troubleshooting-techniques) have not helped resolve the issue. + options: + - label: I have attempted to troubleshoot this already required: true - - type: textarea - attributes: - label: Steps to Reproduce - description: Please write the steps needed to reproduce the bug. - placeholder: | - 1. Go to '...' - 2. Click on '...' - 3. Scroll down to '...' - 4. See error - validations: - required: true + - type: textarea + attributes: + label: Describe the bug + description: Please write a clear and concise description of the bug, including what you expect to happen and what is currently happening. + placeholder: | + Feature '...' is not working properly. I expect '...' to happen, but '...' happens instead. + validations: + required: true - - type: textarea - attributes: - label: Screenshots, screen recording, code snippet - description: | - If possible, please upload a screenshot or screen recording which demonstrates the bug. You can use LIEcap to create a GIF screen recording: https://www.cockos.com/licecap/ - Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. - For small snippets paste it directly here, or you can use GitHub Gist to share multiple code files: https://gist.github.com - Please ensure the shared code can be used by a developer to reproduce the issue—ideally it can be copied into a local development environment or executed in a browser console to help debug the issue - validations: - required: false + - type: textarea + attributes: + label: Steps to Reproduce + description: Please write the steps needed to reproduce the bug. + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error + validations: + required: true - - type: textarea - attributes: - label: Environment information - placeholder: | - - Device: - - OS: - - Docker Desktop version: - - Browser and version: - validations: - required: false + - type: textarea + attributes: + label: Screenshots, screen recording, code snippet + description: | + If possible, please upload a screenshot or screen recording which demonstrates the bug. You can use LIEcap to create a GIF screen recording: https://www.cockos.com/licecap/ + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + For small snippets paste it directly here, or you can use GitHub Gist to share multiple code files: https://gist.github.com + Please ensure the shared code can be used by a developer to reproduce the issue—ideally it can be copied into a local development environment or executed in a browser console to help debug the issue + validations: + required: false - - type: checkboxes - id: terms - attributes: - label: Code of Conduct - description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/Rahmon/wpdocker/blob/develop/CODE_OF_CONDUCT.md). - options: - - label: I agree to follow this project's Code of Conduct - required: true + - type: textarea + attributes: + label: Environment information + placeholder: | + - Device: + - OS: + - Docker Desktop version: + - Browser and version: + validations: + required: false + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/Rahmon/wpdocker/blob/develop/CODE_OF_CONDUCT.md). + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/2-enhancement.yml b/.github/ISSUE_TEMPLATE/2-enhancement.yml index 43cf54ab..48e5959c 100644 --- a/.github/ISSUE_TEMPLATE/2-enhancement.yml +++ b/.github/ISSUE_TEMPLATE/2-enhancement.yml @@ -1,45 +1,45 @@ name: "\U0001F680 Enhancement" -description: "Suggest an idea for this project." -labels: "enhancement" +description: 'Suggest an idea for this project.' +labels: 'enhancement' body: - - type: markdown - attributes: - value: | - Thank you for suggesting an idea to make things better. Please fill in as much of the form below as you can. + - type: markdown + attributes: + value: | + Thank you for suggesting an idea to make things better. Please fill in as much of the form below as you can. - - type: textarea - attributes: - label: Is your enhancement related to a problem? Please describe. - description: Please describe the problem you are trying to solve and your ideal desired behavior. - placeholder: | - I use this project as a `...` and I would like `...` so that `...describe benefit...`. - validations: - required: true + - type: textarea + attributes: + label: Is your enhancement related to a problem? Please describe. + description: Please describe the problem you are trying to solve and your ideal desired behavior. + placeholder: | + I use this project as a `...` and I would like `...` so that `...describe benefit...`. + validations: + required: true - - type: textarea - attributes: - label: Designs - description: | - If applicable, add mockups/screenshots/etc. to help explain your idea. - Tip: You can attach images or videos by clicking this area to highlight it and then dragging files in. - validations: - required: false + - type: textarea + attributes: + label: Designs + description: | + If applicable, add mockups/screenshots/etc. to help explain your idea. + Tip: You can attach images or videos by clicking this area to highlight it and then dragging files in. + validations: + required: false - - type: textarea - attributes: - label: Describe alternatives you've considered - description: | - Please describe alternative solutions or features you have considered. - placeholder: | - I have also considered `...describe alternative...`, however I feel that my solution described above is better because of `...reason...`. - validations: - required: false + - type: textarea + attributes: + label: Describe alternatives you've considered + description: | + Please describe alternative solutions or features you have considered. + placeholder: | + I have also considered `...describe alternative...`, however I feel that my solution described above is better because of `...reason...`. + validations: + required: false - - type: checkboxes - id: terms - attributes: - label: Code of Conduct - description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/Rahmon/wpdocker/blob/develop/CODE_OF_CONDUCT.md). - options: - - label: I agree to follow this project's Code of Conduct - required: true + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/Rahmon/wpdocker/blob/develop/CODE_OF_CONDUCT.md). + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/3-help.yml b/.github/ISSUE_TEMPLATE/3-help.yml index 52e01db4..1788a077 100644 --- a/.github/ISSUE_TEMPLATE/3-help.yml +++ b/.github/ISSUE_TEMPLATE/3-help.yml @@ -1,33 +1,33 @@ -name: "❓ Need help?" -description: "Ask us a question, we are here to help!" -labels: "question" +name: '❓ Need help?' +description: 'Ask us a question, we are here to help!' +labels: 'question' body: - - type: markdown - attributes: - value: | - If you have a question that is neither a bug report nor an enhancement, then please post it here! Please fill in as much of the form below as you can. + - type: markdown + attributes: + value: | + If you have a question that is neither a bug report nor an enhancement, then please post it here! Please fill in as much of the form below as you can. - - type: checkboxes - id: faqs - attributes: - label: Troubleshooting - description: By submitting this question, you agree that our [troubleshooting tips](https://github.com/Rahmon/wpdocker#i-am-having-issues-with-wp-local-docker-what-are-the-best-troubleshooting-techniques) have not helped resolve the quesstion. - options: - - label: I have attempted to troubleshoot this already - required: true - - - type: textarea - attributes: - label: Describe your question - description: A clear and concise description of what your question is. - validations: + - type: checkboxes + id: faqs + attributes: + label: Troubleshooting + description: By submitting this question, you agree that our [troubleshooting tips](https://github.com/Rahmon/wpdocker#i-am-having-issues-with-wp-local-docker-what-are-the-best-troubleshooting-techniques) have not helped resolve the quesstion. + options: + - label: I have attempted to troubleshoot this already required: true - - type: checkboxes - id: terms - attributes: - label: Code of Conduct - description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/Rahmon/wpdocker/blob/develop/CODE_OF_CONDUCT.md). - options: - - label: I agree to follow this project's Code of Conduct - required: true + - type: textarea + attributes: + label: Describe your question + description: A clear and concise description of what your question is. + validations: + required: true + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/Rahmon/wpdocker/blob/develop/CODE_OF_CONDUCT.md). + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9d2f2af4..cb664a92 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - - name: Looking for documentation? - url: https://github.com/Rahmon/wpdocker/#readme - about: If you're looking for information on installation, global commands, environments, WP Snapshots, and related tools then check out the WP Local Docker Readme. + - name: Looking for documentation? + url: https://github.com/Rahmon/wpdocker/#readme + about: If you're looking for information on installation, global commands, environments, WP Snapshots, and related tools then check out the WP Local Docker Readme. diff --git a/.github/workflows/close-stale-issues.yml b/.github/workflows/close-stale-issues.yml index 640e1016..0ece769b 100644 --- a/.github/workflows/close-stale-issues.yml +++ b/.github/workflows/close-stale-issues.yml @@ -33,4 +33,3 @@ jobs: close-issue-reason: 'not_planned' any-of-labels: 'reporter feedback' remove-stale-when-updated: true - diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..3c8a3d15 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,20 @@ +# Dependencies & coverage +node_modules +coverage + +# Lock / generated files +package-lock.json +skills-lock.json + +# Husky internals (regenerated on install) +.husky/_ + +# Local agent tooling +.agents +.claude + +# Markdown is hand-formatted (e.g. README); opt in by removing this line +*.md + +# nginx error-page templates (intentionally minimal HTML) +*.html diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..8ba21e8f --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,20 @@ +{ + "useTabs": true, + "tabWidth": 4, + "printWidth": 100, + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf", + "overrides": [ + { + "files": ["*.yml", "*.yaml"], + "options": { + "useTabs": false, + "tabWidth": 2 + } + } + ] +} diff --git a/global/docker-compose.yml b/global/docker-compose.yml index 01f8a37f..5a6c8b84 100644 --- a/global/docker-compose.yml +++ b/global/docker-compose.yml @@ -1,4 +1,4 @@ -version: "2.4" +version: '2.4' services: # Run a local dnsmasq server for the containers that will resolve DNS queries and # route any *.test domains to the gateway container diff --git a/hosts.js b/hosts.js index 99f89028..7c18fdb9 100755 --- a/hosts.js +++ b/hosts.js @@ -1,59 +1,47 @@ #!/usr/bin/env node -const yargs = require( 'yargs' ); -const hostile = require( 'hostile' ); +const yargs = require('yargs'); +const hostile = require('hostile'); -function options( yargs ) { - yargs.positional( 'hosts', { +function options(yargs) { + yargs.positional('hosts', { describe: 'A host domain name.', type: 'string', - } ); + }); } -const IPs = [ '::1', '127.0.0.1' ]; +const IPs = ['::1', '127.0.0.1']; -function add( { hosts } ) { +function add({ hosts }) { async function run() { try { - for ( const ip of IPs ) { - await new Promise( - ( resolve,reject ) => { - hostile.set( - ip, - hosts.join( ' ' ), - err => err ? reject( err ) : resolve() - ); - } - ); + for (const ip of IPs) { + await new Promise((resolve, reject) => { + hostile.set(ip, hosts.join(' '), (err) => (err ? reject(err) : resolve())); + }); } - console.log( 'Added to hosts file successfully!' ); - } catch ( err ) { - console.error( err.message ); - process.exit( err.errno ); + console.log('Added to hosts file successfully!'); + } catch (err) { + console.error(err.message); + process.exit(err.errno); } } run(); } -function remove( { hosts } ) { +function remove({ hosts }) { async function run() { try { - for ( const ip of IPs ) { - await new Promise( - ( resolve,reject ) => { - hostile.remove( - ip, - hosts.join( ' ' ), - err => err ? reject( err ) : resolve() - ); - } - ); + for (const ip of IPs) { + await new Promise((resolve, reject) => { + hostile.remove(ip, hosts.join(' '), (err) => (err ? reject(err) : resolve())); + }); } - console.log( 'Added to hosts file successfully!' ); - } catch ( err ) { - console.error( err.message ); - process.exit( err.errno ); + console.log('Added to hosts file successfully!'); + } catch (err) { + console.error(err.message); + process.exit(err.errno); } } @@ -61,14 +49,14 @@ function remove( { hosts } ) { } // usage and help flag -yargs.scriptName( 'wpdocker-hosts' ); -yargs.usage( 'Usage: wpdocker-hosts ' ); -yargs.help( 'h' ); -yargs.alias( 'h', 'help' ); +yargs.scriptName('wpdocker-hosts'); +yargs.usage('Usage: wpdocker-hosts '); +yargs.help('h'); +yargs.alias('h', 'help'); // commands -yargs.command( 'add ', 'Add new hosts to the hosts file.', options, add ); -yargs.command( 'remove ', 'Remove hosts from the hosts file.', options, remove ); +yargs.command('add ', 'Add new hosts to the hosts file.', options, add); +yargs.command('remove ', 'Remove hosts from the hosts file.', options, remove); // parse and process CLI args yargs.demandCommand(); diff --git a/index.js b/index.js index e73e535e..258a3b6f 100755 --- a/index.js +++ b/index.js @@ -1,14 +1,14 @@ #!/usr/bin/env node -const yargs = require( 'yargs' ); +const yargs = require('yargs'); -const { checkIfConfigured, configureDefaults } = require( './src/configure' ); -const { checkForUpdates } = require( './src/command-utils' ); +const { checkIfConfigured, configureDefaults } = require('./src/configure'); +const { checkForUpdates } = require('./src/command-utils'); async function bootstrap() { // check configuration const configured = await checkIfConfigured(); - if ( configured === false ) { + if (configured === false) { await configureDefaults(); } @@ -16,28 +16,28 @@ async function bootstrap() { await checkForUpdates(); // usage and help flag - yargs.scriptName( 'wpdocker' ); - yargs.usage( 'Usage: wpdocker ' ); - yargs.wrap( Math.min( 150, yargs.terminalWidth() ) ); - yargs.help( 'h' ); - yargs.alias( 'h', 'help' ); - yargs.alias( 'v', 'version' ); + yargs.scriptName('wpdocker'); + yargs.usage('Usage: wpdocker '); + yargs.wrap(Math.min(150, yargs.terminalWidth())); + yargs.help('h'); + yargs.alias('h', 'help'); + yargs.alias('v', 'version'); // global options - yargs.option( 'verbose', { + yargs.option('verbose', { description: 'Display extended output', default: false, type: 'boolean', - } ); + }); - yargs.option( 'env', { + yargs.option('env', { description: 'Environment name', default: false, type: 'string', - } ); + }); // define commands, parse and process CLI args - yargs.commandDir( 'src/commands' ); + yargs.commandDir('src/commands'); yargs.demandCommand(); yargs.parse(); } diff --git a/jest.config.js b/jest.config.js index 34936640..b527ab15 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,7 @@ module.exports = { testEnvironment: 'node', - testMatch: [ '**/test/**/*.test.js' ], - collectCoverageFrom: [ 'src/**/*.js' ], + testMatch: ['**/test/**/*.test.js'], + collectCoverageFrom: ['src/**/*.js'], coverageDirectory: 'coverage', clearMocks: true, }; diff --git a/jsconfig.json b/jsconfig.json index 3b67b9d4..a50ed6aa 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -2,8 +2,5 @@ "compilerOptions": { "checkJs": true }, - "include": [ - "src/*.js", - "src/**/*.js" - ] -} \ No newline at end of file + "include": ["src/*.js", "src/**/*.js"] +} diff --git a/package-lock.json b/package-lock.json index 889695ea..1895e846 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,9 +46,11 @@ "@10up/eslint-config": "^1.0.9", "babel-eslint": "^10.0.3", "eslint": "^7.32.0", + "eslint-config-prettier": "^9.1.2", "husky": "^9.1.7", "jest": "^29.7.0", - "lint-staged": "^10.5.4" + "lint-staged": "^10.5.4", + "prettier": "^3.8.3" }, "engines": { "node": ">=18" @@ -2471,6 +2473,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -5144,6 +5159,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -8207,6 +8238,13 @@ } } }, + "eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "requires": {} + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -10194,6 +10232,12 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true + }, "pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", diff --git a/package.json b/package.json index 643613ba..ebb76bfd 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "wpdocker-hosts": "./hosts.js", "prepare": "husky", "lint": "eslint . --ext .js --ignore-pattern node_modules", - "format": "npm run lint --silent -- --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", "test": "jest", "test:unit": "jest test/unit", "test:integration": "jest test/integration", @@ -89,11 +90,17 @@ "@10up/eslint-config": "^1.0.9", "babel-eslint": "^10.0.3", "eslint": "^7.32.0", + "eslint-config-prettier": "^9.1.2", "husky": "^9.1.7", "jest": "^29.7.0", - "lint-staged": "^10.5.4" + "lint-staged": "^10.5.4", + "prettier": "^3.8.3" }, "lint-staged": { - "*.js": "eslint --fix" + "*.js": [ + "eslint --fix", + "prettier --write" + ], + "*.{json,yml,yaml}": "prettier --write" } } diff --git a/src/certificates.js b/src/certificates.js index 2e52f59d..d2861381 100644 --- a/src/certificates.js +++ b/src/certificates.js @@ -1,25 +1,25 @@ -const { execSync } = require( 'child_process' ); -const { join } = require( 'path' ); -const { readFile, writeFile } = require( 'fs' ).promises; +const { execSync } = require('child_process'); +const { join } = require('path'); +const { readFile, writeFile } = require('fs').promises; -const mkcert = require( 'mkcert' ); -const mkcertPrebuilt = require( 'mkcert-prebuilt' ); +const mkcert = require('mkcert'); +const mkcertPrebuilt = require('mkcert-prebuilt'); -const envUtil = require( './env-utils' ); -const { getSslCertsDirectory } = require( './configure' ); +const envUtil = require('./env-utils'); +const { getSslCertsDirectory } = require('./configure'); function getCARoot() { - return execSync( `"${ mkcertPrebuilt }" -CAROOT`, { encoding: 'utf-8' } ).trim(); + return execSync(`"${mkcertPrebuilt}" -CAROOT`, { encoding: 'utf-8' }).trim(); } -function installCA( verbose = false ) { +function installCA(verbose = false) { try { - execSync( `"${ mkcertPrebuilt }" -install`, { + execSync(`"${mkcertPrebuilt}" -install`, { stdio: verbose ? 'inherit' : 'ignore', - } ); - } catch ( err ) { - if ( verbose ) { - console.error( err ); + }); + } catch (err) { + if (verbose) { + console.error(err); } return false; @@ -28,28 +28,28 @@ function installCA( verbose = false ) { return true; } -async function generate( envName, hosts ) { - const envSlug = envUtil.envSlug( envName ); - const allHosts = [ ...hosts, ...hosts.map( ( host ) => `*.${ host }` ) ]; +async function generate(envName, hosts) { + const envSlug = envUtil.envSlug(envName); + const allHosts = [...hosts, ...hosts.map((host) => `*.${host}`)]; const caRoot = getCARoot(); - const caKey = await readFile( join( caRoot, 'rootCA-key.pem' ), { encoding: 'utf-8' } ); - const caCert = await readFile( join( caRoot, 'rootCA.pem' ), { encoding: 'utf-8' } ); + const caKey = await readFile(join(caRoot, 'rootCA-key.pem'), { encoding: 'utf-8' }); + const caCert = await readFile(join(caRoot, 'rootCA.pem'), { encoding: 'utf-8' }); - const cert = await mkcert.createCert( { + const cert = await mkcert.createCert({ caCert, caKey, domains: allHosts, validityDays: 365, - } ); + }); const sslDir = await getSslCertsDirectory(); - const filename = join( sslDir, envSlug ); - const certFile = `${ filename }.crt`; - const keyFile = `${ filename }.key`; + const filename = join(sslDir, envSlug); + const certFile = `${filename}.crt`; + const keyFile = `${filename}.key`; - await writeFile( certFile, cert.cert ); - await writeFile( keyFile, cert.key ); + await writeFile(certFile, cert.cert); + await writeFile(keyFile, cert.key); return { cert: certFile, diff --git a/src/command-utils.js b/src/command-utils.js index d48e4ecf..a7c8585e 100644 --- a/src/command-utils.js +++ b/src/command-utils.js @@ -1,21 +1,21 @@ -const path = require( 'path' ); -const { execSync } = require( 'child_process' ); +const path = require('path'); +const { execSync } = require('child_process'); -const updateCheck = require( 'update-check' ); -const chalk = require( 'chalk' ); +const updateCheck = require('update-check'); +const chalk = require('chalk'); -const envUtils = require( './env-utils' ); +const envUtils = require('./env-utils'); exports.checkIfDockerRunning = function () { let output; try { - output = execSync( 'docker system info' ); + output = execSync('docker system info'); } catch { return false; } - if ( output.toString().toLowerCase().indexOf( 'version' ) === -1 ) { + if (output.toString().toLowerCase().indexOf('version') === -1) { return false; } @@ -23,16 +23,24 @@ exports.checkIfDockerRunning = function () { }; exports.checkForUpdates = async function () { - const pkg = require( path.join( envUtils.rootPath, 'package' ) ); + const pkg = require(path.join(envUtils.rootPath, 'package')); let update = null; try { - update = await updateCheck( pkg ); - } catch ( err ) { - console.error( chalk.yellow( 'Failed to automatically check for updates. Please ensure WP Docker is up to date.' ) ); + update = await updateCheck(pkg); + } catch (err) { + console.error( + chalk.yellow( + 'Failed to automatically check for updates. Please ensure WP Docker is up to date.', + ), + ); } - if ( update ) { - console.warn( chalk.yellow( `WP Docker version ${ update.latest } is now available. Please run \`npm i -g @wpdocker/wpdocker\` to update!` ) ); + if (update) { + console.warn( + chalk.yellow( + `WP Docker version ${update.latest} is now available. Please run \`npm i -g @wpdocker/wpdocker\` to update!`, + ), + ); } }; diff --git a/src/commands/cache.js b/src/commands/cache.js index ed24850c..e3254d42 100644 --- a/src/commands/cache.js +++ b/src/commands/cache.js @@ -1,6 +1,6 @@ exports.command = 'cache '; exports.desc = 'Manages the build cache.'; -exports.builder = ( yargs ) => { - yargs.commandDir( 'cache' ); +exports.builder = (yargs) => { + yargs.commandDir('cache'); }; diff --git a/src/commands/cache/clear.js b/src/commands/cache/clear.js index b7e22286..bebd0c69 100644 --- a/src/commands/cache/clear.js +++ b/src/commands/cache/clear.js @@ -1,19 +1,19 @@ -const { removeCacheVolume, ensureCacheExists } = require( '../../gateway' ); -const makeDocker = require( '../../utils/make-docker' ); -const makeCommand = require( '../../utils/make-command' ); -const makeSpinner = require( '../../utils/make-spinner' ); +const { removeCacheVolume, ensureCacheExists } = require('../../gateway'); +const makeDocker = require('../../utils/make-docker'); +const makeCommand = require('../../utils/make-command'); +const makeSpinner = require('../../utils/make-spinner'); exports.command = 'clear'; exports.desc = 'Clears npm, wp-cli, and WP Snapshots caches.'; -exports.handler = makeCommand( {}, async ( { verbose } ) => { +exports.handler = makeCommand({}, async ({ verbose }) => { const docker = makeDocker(); - const spinner = ! verbose ? makeSpinner() : undefined; + const spinner = !verbose ? makeSpinner() : undefined; - await removeCacheVolume( docker, spinner ); - await ensureCacheExists( docker, spinner ); + await removeCacheVolume(docker, spinner); + await ensureCacheExists(docker, spinner); - if ( ! spinner ) { - console.log( 'Cache Cleared' ); + if (!spinner) { + console.log('Cache Cleared'); } -} ); +}); diff --git a/src/commands/cert.js b/src/commands/cert.js index 5520aebf..91659a95 100644 --- a/src/commands/cert.js +++ b/src/commands/cert.js @@ -1,6 +1,6 @@ exports.command = 'cert '; exports.desc = 'Manages certificates.'; -exports.builder = ( yargs ) => { - yargs.commandDir( 'cert' ); +exports.builder = (yargs) => { + yargs.commandDir('cert'); }; diff --git a/src/commands/cert/generate.js b/src/commands/cert/generate.js index 16402763..c5d17360 100644 --- a/src/commands/cert/generate.js +++ b/src/commands/cert/generate.js @@ -1,110 +1,115 @@ -const { join } = require( 'path' ); +const { join } = require('path'); -const inquirer = require( 'inquirer' ); +const inquirer = require('inquirer'); -const envUtils = require( '../../env-utils' ); -const { generate } = require( '../../certificates' ); -const { readYaml, writeYaml } = require( '../../utils/yaml' ); -const makeCommand = require( '../../utils/make-command' ); -const makeSpinner = require( '../../utils/make-spinner' ); +const envUtils = require('../../env-utils'); +const { generate } = require('../../certificates'); +const { readYaml, writeYaml } = require('../../utils/yaml'); +const makeCommand = require('../../utils/make-command'); +const makeSpinner = require('../../utils/make-spinner'); exports.command = 'generate '; exports.desc = 'Generates SSL certificates for given domains.'; -exports.builder = function( yargs ) { - yargs.positional( 'domains', { +exports.builder = function (yargs) { + yargs.positional('domains', { describe: 'Domains to include in the certificate', type: 'string', - } ); + }); }; -exports.handler = makeCommand( { checkDocker: false }, async ( { domains, env, verbose } ) => { - const spinner = ! verbose ? makeSpinner() : undefined; - const envName = await envUtils.resolveEnvironment( env ); - const slug = envUtils.envSlug( envName ); +exports.handler = makeCommand({ checkDocker: false }, async ({ domains, env, verbose }) => { + const spinner = !verbose ? makeSpinner() : undefined; + const envName = await envUtils.resolveEnvironment(env); + const slug = envUtils.envSlug(envName); - const certs = await generateCert( slug, domains, spinner ); - if ( certs ) { - const envPath = await envUtils.envPath( slug ); - await updateConfig( envPath, certs ); - await checkDockerCompose( envPath, slug, spinner ); + const certs = await generateCert(slug, domains, spinner); + if (certs) { + const envPath = await envUtils.envPath(slug); + await updateConfig(envPath, certs); + await checkDockerCompose(envPath, slug, spinner); } -} ); +}); -async function generateCert( slug, domains, spinner ) { - if ( spinner ) { - spinner.start( 'Generating certificates...' ); +async function generateCert(slug, domains, spinner) { + if (spinner) { + spinner.start('Generating certificates...'); } else { - console.log( 'Generating certificates:' ); + console.log('Generating certificates:'); } try { - const certs = await generate( slug, domains ); - if ( certs ) { - if ( spinner ) { - spinner.succeed( 'Certificates are generated...' ); + const certs = await generate(slug, domains); + if (certs) { + if (spinner) { + spinner.succeed('Certificates are generated...'); } else { - console.log( ' - Done' ); + console.log(' - Done'); } } return certs; - } catch ( err ) { - if ( spinner ) { - spinner.fail( `Certificates generation failed... ${ err.toString() }` ); + } catch (err) { + if (spinner) { + spinner.fail(`Certificates generation failed... ${err.toString()}`); } else { - console.log( ` - Failed: ${ err.toString() }` ); + console.log(` - Failed: ${err.toString()}`); } } return null; } -async function updateConfig( envPath, certs ) { - const config = await envUtils.getEnvConfig( envPath ); +async function updateConfig(envPath, certs) { + const config = await envUtils.getEnvConfig(envPath); config.certs = certs; - await envUtils.saveEnvConfig( envPath, config ); + await envUtils.saveEnvConfig(envPath, config); } -async function checkDockerCompose( envPath, slug, spinner ) { - const filename = join( envPath, 'docker-compose.yml' ); - const yaml = readYaml( filename ); +async function checkDockerCompose(envPath, slug, spinner) { + const filename = join(envPath, 'docker-compose.yml'); + const yaml = readYaml(filename); - if ( ! yaml || ! yaml.services || ! yaml.services.nginx ) { - if ( spinner ) { - spinner.warn( 'Environment\'s docker-compose.yml file hasn\'t been found or it doesn\'t contain nginx service...' ); + if (!yaml || !yaml.services || !yaml.services.nginx) { + if (spinner) { + spinner.warn( + "Environment's docker-compose.yml file hasn't been found or it doesn't contain nginx service...", + ); } else { - console.warn( 'Environment\'s docker-compose.yml file hasn\'t been found or it doesn\'t contain nginx service.' ); + console.warn( + "Environment's docker-compose.yml file hasn't been found or it doesn't contain nginx service.", + ); } return; } - if ( ! yaml.services.nginx.environment ) { + if (!yaml.services.nginx.environment) { yaml.services.nginx.environment = {}; } - if ( yaml.services.nginx.environment.CERT_NAME === slug ) { + if (yaml.services.nginx.environment.CERT_NAME === slug) { return; } - const { confirm } = await inquirer.prompt( { + const { confirm } = await inquirer.prompt({ name: 'confirm', type: 'confirm', - message: 'Do you want to update environment\'s docker-compose.yml file to use the new certificate?', + message: + "Do you want to update environment's docker-compose.yml file to use the new certificate?", default: true, - } ); + }); - if ( ! confirm ) { + if (!confirm) { return; } yaml.services.nginx.environment.CERT_NAME = slug; - await writeYaml( filename, yaml ); + await writeYaml(filename, yaml); - if ( spinner ) { - spinner.succeed( 'Docker compose file has been updated...' ); + if (spinner) { + spinner.succeed('Docker compose file has been updated...'); } else { - console.log( 'Docker compose file has been updated.' ); + console.log('Docker compose file has been updated.'); } } diff --git a/src/commands/cert/install.js b/src/commands/cert/install.js index 59b8f0f4..31db1fb2 100644 --- a/src/commands/cert/install.js +++ b/src/commands/cert/install.js @@ -1,8 +1,8 @@ -const { installCA } = require( '../../certificates' ); +const { installCA } = require('../../certificates'); exports.command = 'install'; exports.desc = 'Installs a new local CA in the system trust store.'; -exports.handler = function() { - installCA( true ); +exports.handler = function () { + installCA(true); }; diff --git a/src/commands/completion.js b/src/commands/completion.js index bc1e6fee..3aa7207b 100644 --- a/src/commands/completion.js +++ b/src/commands/completion.js @@ -1,41 +1,47 @@ -const { resolve } = require( 'path' ); +const { resolve } = require('path'); -const chalk = require( 'chalk' ); +const chalk = require('chalk'); exports.command = 'completion '; exports.desc = 'Displays completion script for selected shell.'; -exports.builder = function ( yargs ) { - yargs.positional( 'shell', { +exports.builder = function (yargs) { + yargs.positional('shell', { describe: 'A shell to display completion script for.', type: 'string', - } ); + }); }; -exports.handler = ( { shell } ) => { - switch ( shell ) { +exports.handler = ({ shell }) => { + switch (shell) { case 'bash': { - const filename = resolve( __dirname, '../../scripts/wpdocker-completion.bash' ); - console.log( '#' ); - console.log( '# wp-local-docker command completion script' ); - console.log( '#' ); - console.log( `# Installation: ${ chalk.bold.cyan( 'wpdocker completion bash >> ~/.bashrc' ) }` ); - console.log( `# or ${ chalk.bold.cyan( 'wpdocker completion bash >> ~/.bash_profile' ) } on OSX.` ); - console.log( '#' ); - console.log( `source ${ filename }` ); + const filename = resolve(__dirname, '../../scripts/wpdocker-completion.bash'); + console.log('#'); + console.log('# wp-local-docker command completion script'); + console.log('#'); + console.log( + `# Installation: ${chalk.bold.cyan('wpdocker completion bash >> ~/.bashrc')}`, + ); + console.log( + `# or ${chalk.bold.cyan('wpdocker completion bash >> ~/.bash_profile')} on OSX.`, + ); + console.log('#'); + console.log(`source ${filename}`); break; } case 'zsh': { - const filename = resolve( __dirname, '../../scripts/wpdocker-completion.zsh' ); - console.log( '###-begin wp-local-docker command completion script for zsh-###' ); - console.log( `# Installation: ${ chalk.bold.cyan( 'wpdocker completion zsh >> ~/.zshrc' ) }` ); - console.log( '###-end wp-local-docker command completion script for zsh-###' ); - console.log( `source ${ filename }` ); + const filename = resolve(__dirname, '../../scripts/wpdocker-completion.zsh'); + console.log('###-begin wp-local-docker command completion script for zsh-###'); + console.log( + `# Installation: ${chalk.bold.cyan('wpdocker completion zsh >> ~/.zshrc')}`, + ); + console.log('###-end wp-local-docker command completion script for zsh-###'); + console.log(`source ${filename}`); break; } default: - console.error( `${ chalk.bold.redBright( shell ) } ${ chalk.red( 'shell is not supported.' ) }` ); - process.exit( 1 ); + console.error(`${chalk.bold.redBright(shell)} ${chalk.red('shell is not supported.')}`); + process.exit(1); break; } }; diff --git a/src/commands/configure.js b/src/commands/configure.js index f6ac0032..2be29e94 100644 --- a/src/commands/configure.js +++ b/src/commands/configure.js @@ -1,29 +1,24 @@ -const os = require( 'os' ); -const path = require( 'path' ); +const os = require('os'); +const path = require('path'); -const inquirer = require( 'inquirer' ); -const fs = require( 'fs-extra' ); +const inquirer = require('inquirer'); +const fs = require('fs-extra'); -const makeCommand = require( '../utils/make-command' ); -const { validateNotEmpty } = require( '../prompt-validators' ); -const { - configure, - get, - getConfigDirectory, - getDefaults, -} = require( '../configure' ); +const makeCommand = require('../utils/make-command'); +const { validateNotEmpty } = require('../prompt-validators'); +const { configure, get, getConfigDirectory, getDefaults } = require('../configure'); exports.command = 'configure'; exports.desc = 'Set up a configuration for WP Docker.'; -exports.handler = makeCommand( { checkDocker: false }, async () => { +exports.handler = makeCommand({ checkDocker: false }, async () => { const defaults = getDefaults(); - const currentDir = await get( 'sitesPath' ); - const currentHosts = await get( 'manageHosts' ); - const currentSnapshots = await get( 'snapshotsPath' ); + const currentDir = await get('sitesPath'); + const currentHosts = await get('manageHosts'); + const currentSnapshots = await get('snapshotsPath'); - const resolveHome = ( input ) => input.replace( '~', os.homedir() ); + const resolveHome = (input) => input.replace('~', os.homedir()); const questions = [ { @@ -52,18 +47,17 @@ exports.handler = makeCommand( { checkDocker: false }, async () => { }, ]; - if ( fs.existsSync( path.join( getConfigDirectory(), 'global' ) ) ) { - questions.push( - { - name: 'overwriteGlobal', - type: 'confirm', - message: 'Do you want to reset your global services configuration? This will reset any customizations you have made.', - default: false - } - ); + if (fs.existsSync(path.join(getConfigDirectory(), 'global'))) { + questions.push({ + name: 'overwriteGlobal', + type: 'confirm', + message: + 'Do you want to reset your global services configuration? This will reset any customizations you have made.', + default: false, + }); } - const answers = await inquirer.prompt( questions ); + const answers = await inquirer.prompt(questions); - await configure( { ...defaults, ...answers } ); -} ); + await configure({ ...defaults, ...answers }); +}); diff --git a/src/commands/create.js b/src/commands/create.js index 249cf6a8..41dd3663 100644 --- a/src/commands/create.js +++ b/src/commands/create.js @@ -1,33 +1,33 @@ -const { EOL } = require( 'os' ); - -const inquirer = require( 'inquirer' ); -const fsExtra = require( 'fs-extra' ); -const sudo = require( '@vscode/sudo-prompt' ); -const which = require( 'which' ); - -const { startGlobal } = require( '../gateway' ); -const environment = require( '../environment' ); -const envUtils = require( '../env-utils' ); - -const makeSpinner = require( '../utils/make-spinner' ); -const makeCommand = require( '../utils/make-command' ); -const makeBoxen = require( '../utils/make-boxen' ); -const { replaceLinks } = require( '../utils/make-link' ); - -const makeInquirer = require( './create/inquirer' ); -const makeDockerCompose = require( './create/make-docker-compose' ); -const makeFs = require( './create/make-fs' ); -const makeSaveYamlFile = require( './create/save-yaml-file' ); -const makeCopyConfigs = require( './create/copy-configs' ); -const makeDatabase = require( './create/create-database' ); -const makeInstallWordPress = require( './create/install-wordpress' ); -const makeSaveJsonFile = require( './create/save-json-file' ); -const makeUpdateHosts = require( './create/update-hosts' ); -const makeCert = require( './create/make-cert' ); - -async function createCommand( spinner, defaults = {} ) { - const answers = await makeInquirer( inquirer )( defaults ); - await envUtils.checkForEOLPHP( answers.php ); +const { EOL } = require('os'); + +const inquirer = require('inquirer'); +const fsExtra = require('fs-extra'); +const sudo = require('@vscode/sudo-prompt'); +const which = require('which'); + +const { startGlobal } = require('../gateway'); +const environment = require('../environment'); +const envUtils = require('../env-utils'); + +const makeSpinner = require('../utils/make-spinner'); +const makeCommand = require('../utils/make-command'); +const makeBoxen = require('../utils/make-boxen'); +const { replaceLinks } = require('../utils/make-link'); + +const makeInquirer = require('./create/inquirer'); +const makeDockerCompose = require('./create/make-docker-compose'); +const makeFs = require('./create/make-fs'); +const makeSaveYamlFile = require('./create/save-yaml-file'); +const makeCopyConfigs = require('./create/copy-configs'); +const makeDatabase = require('./create/create-database'); +const makeInstallWordPress = require('./create/install-wordpress'); +const makeSaveJsonFile = require('./create/save-json-file'); +const makeUpdateHosts = require('./create/update-hosts'); +const makeCert = require('./create/make-cert'); + +async function createCommand(spinner, defaults = {}) { + const answers = await makeInquirer(inquirer)(defaults); + await envUtils.checkForEOLPHP(answers.php); const settings = { ...answers, @@ -36,61 +36,66 @@ async function createCommand( spinner, defaults = {} ) { certs: {}, }; - const hostname = Array.isArray( settings.domain ) ? settings.domain[0] : settings.domain; - const envHosts = Array.isArray( settings.domain ) ? settings.domain : [ settings.domain ]; + const hostname = Array.isArray(settings.domain) ? settings.domain[0] : settings.domain; + const envHosts = Array.isArray(settings.domain) ? settings.domain : [settings.domain]; - settings.envSlug = envUtils.envSlug( hostname ); - settings.paths = await makeFs( spinner )( hostname ); - const saveYaml = makeSaveYamlFile( settings['paths']['/'] ); - settings.certs = await makeCert( spinner )( settings.envSlug, envHosts ); + settings.envSlug = envUtils.envSlug(hostname); + settings.paths = await makeFs(spinner)(hostname); + const saveYaml = makeSaveYamlFile(settings['paths']['/']); + settings.certs = await makeCert(spinner)(settings.envSlug, envHosts); - const dockerComposer = await makeDockerCompose( spinner )( envHosts, settings ); - await saveYaml( 'docker-compose.yml', dockerComposer ); - await saveYaml( 'wp-cli.yml', { ssh: 'docker-compose:phpfpm' } ); + const dockerComposer = await makeDockerCompose(spinner)(envHosts, settings); + await saveYaml('docker-compose.yml', dockerComposer); + await saveYaml('wp-cli.yml', { ssh: 'docker-compose:phpfpm' }); - await makeCopyConfigs( spinner, fsExtra )( settings ); + await makeCopyConfigs(spinner, fsExtra)(settings); - await startGlobal( spinner ); - await makeDatabase( spinner )( settings.envSlug ); - await environment.start( settings.envSlug, spinner ); + await startGlobal(spinner); + await makeDatabase(spinner)(settings.envSlug); + await environment.start(settings.envSlug, spinner); - await makeInstallWordPress( spinner )( hostname, settings ); + await makeInstallWordPress(spinner)(hostname, settings); - await makeSaveJsonFile( settings['paths']['/'] )( '.config.json', { envHosts, certs: settings['certs'] } ); - await makeUpdateHosts( which, sudo, spinner )( envHosts ); + await makeSaveJsonFile(settings['paths']['/'])('.config.json', { + envHosts, + certs: settings['certs'], + }); + await makeUpdateHosts(which, sudo, spinner)(envHosts); return settings; } exports.command = 'create'; exports.desc = 'Create a new docker environment.'; -exports.aliases = [ 'new' ]; +exports.aliases = ['new']; -exports.handler = makeCommand( async ( { verbose } ) => { - const spinner = ! verbose ? makeSpinner() : undefined; - const answers = await createCommand( spinner, {} ); +exports.handler = makeCommand(async ({ verbose }) => { + const spinner = !verbose ? makeSpinner() : undefined; + const answers = await createCommand(spinner, {}); - if ( spinner && !! answers.wordpress && answers.wordpress.type === 'subdomain' ) { - spinner.info( 'Note: Subdomain multisites require any additional subdomains to be added manually to your hosts file!' ); + if (spinner && !!answers.wordpress && answers.wordpress.type === 'subdomain') { + spinner.info( + 'Note: Subdomain multisites require any additional subdomains to be added manually to your hosts file!', + ); } - let info = `Successfully Created Site!${ EOL }${ EOL }`; + let info = `Successfully Created Site!${EOL}${EOL}`; const links = {}; const http = answers.certs ? 'https' : 'http'; - ( Array.isArray( answers.domain ) ? answers.domain : [ answers.domain ] ).forEach( ( host ) => { - const home = `${ http }://${ host }/`; - const admin = `${ http }://${ host }/wp-admin/`; + (Array.isArray(answers.domain) ? answers.domain : [answers.domain]).forEach((host) => { + const home = `${http}://${host}/`; + const admin = `${http}://${host}/wp-admin/`; - links[ home ] = home; - links[ admin ] = admin; + links[home] = home; + links[admin] = admin; - info += `Homepage: ${ home }${ EOL }`; - info += `WP admin: ${ admin }${ EOL }`; + info += `Homepage: ${home}${EOL}`; + info += `WP admin: ${admin}${EOL}`; info += EOL; - } ); + }); - console.log( replaceLinks( makeBoxen()( info ), links ) ); -} ); + console.log(replaceLinks(makeBoxen()(info), links)); +}); exports.createCommand = createCommand; diff --git a/src/commands/create/copy-configs.js b/src/commands/create/copy-configs.js index 821f452f..f7a0e44b 100644 --- a/src/commands/create/copy-configs.js +++ b/src/commands/create/copy-configs.js @@ -1,36 +1,34 @@ -const { join } = require( 'path' ); -const { readFile, writeFile } = require( 'fs' ).promises; +const { join } = require('path'); +const { readFile, writeFile } = require('fs').promises; -const { srcPath } = require( '../../env-utils' ); -const { createProxyConfig } = require( '../../configure' ); +const { srcPath } = require('../../env-utils'); +const { createProxyConfig } = require('../../configure'); -async function updateConfig( envPath, name, cb ) { - const nginxConfigPath = join( envPath, 'config', 'nginx', name ); - const curConfig = await readFile( nginxConfigPath, { encoding: 'utf-8' } ); - await writeFile( nginxConfigPath, cb( curConfig ) ); +async function updateConfig(envPath, name, cb) { + const nginxConfigPath = join(envPath, 'config', 'nginx', name); + const curConfig = await readFile(nginxConfigPath, { encoding: 'utf-8' }); + await writeFile(nginxConfigPath, cb(curConfig)); } -module.exports = function makeCopyConfigs( spinner, { copy } ) { - return async ( { paths, mediaProxy, domain } ) => { +module.exports = function makeCopyConfigs(spinner, { copy }) { + return async ({ paths, mediaProxy, domain }) => { const envPath = paths['/']; - await copy( join( srcPath, 'config' ), join( envPath, 'config' ) ); - await copy( join( srcPath, 'containers' ), join( envPath, '.containers' ) ); + await copy(join(srcPath, 'config'), join(envPath, 'config')); + await copy(join(srcPath, 'containers'), join(envPath, '.containers')); - await updateConfig( - envPath, - 'develop.conf', - ( config ) => config.replace( '#{MAIN_DOMAIN}', Array.isArray( domain ) ? domain[0] : domain ), + await updateConfig(envPath, 'develop.conf', (config) => + config.replace('#{MAIN_DOMAIN}', Array.isArray(domain) ? domain[0] : domain), ); - if ( mediaProxy ) { - await updateConfig( envPath, 'server.conf', createProxyConfig.bind( null, mediaProxy ) ); + if (mediaProxy) { + await updateConfig(envPath, 'server.conf', createProxyConfig.bind(null, mediaProxy)); } - if ( spinner ) { - spinner.succeed( 'Configuration files are copied...' ); + if (spinner) { + spinner.succeed('Configuration files are copied...'); } else { - console.log( 'Copied configuration files.' ); + console.log('Copied configuration files.'); } }; }; diff --git a/src/commands/create/create-database.js b/src/commands/create/create-database.js index c9bb2ff9..26ee824a 100644 --- a/src/commands/create/create-database.js +++ b/src/commands/create/create-database.js @@ -1,20 +1,20 @@ -const database = require( '../../database' ); +const database = require('../../database'); -module.exports = function makeDatabase( spinner ) { - return async ( envSlug ) => { - if ( spinner ) { - spinner.start( 'Creating database...' ); +module.exports = function makeDatabase(spinner) { + return async (envSlug) => { + if (spinner) { + spinner.start('Creating database...'); } else { - console.log( 'Creating database:' ); + console.log('Creating database:'); } - await database.create( envSlug ); - await database.assignPrivs( envSlug ); + await database.create(envSlug); + await database.assignPrivs(envSlug); - if ( spinner ) { - spinner.succeed( 'Database is created...' ); + if (spinner) { + spinner.succeed('Database is created...'); } else { - console.log( ' - Done' ); + console.log(' - Done'); } }; }; diff --git a/src/commands/create/inquirer.js b/src/commands/create/inquirer.js index cea7d803..03cad453 100644 --- a/src/commands/create/inquirer.js +++ b/src/commands/create/inquirer.js @@ -1,18 +1,7 @@ -const { validateNotEmpty, parseHostname, parseProxyUrl } = require( '../../prompt-validators' ); -const { createDefaultProxy } = require( '../../env-utils' ); - -const phpVersions = [ - '8.3', - '8.2', - '8.1', - '8.0', - '7.4', - '7.3', - '7.2', - '7.1', - '7.0', - '5.6', -]; +const { validateNotEmpty, parseHostname, parseProxyUrl } = require('../../prompt-validators'); +const { createDefaultProxy } = require('../../env-utils'); + +const phpVersions = ['8.3', '8.2', '8.1', '8.0', '7.4', '7.3', '7.2', '7.1', '7.0', '5.6']; const wordpressTypes = [ { name: 'Single Site', value: 'single' }, @@ -20,65 +9,58 @@ const wordpressTypes = [ { name: 'Subdomain Multisite', value: 'subdomain' }, ]; -function defaultIsUndefined( val ) { +function defaultIsUndefined(val) { return () => typeof val === 'undefined'; } -function marshalDomains( original, { hostname, extraHosts } ) { +function marshalDomains(original, { hostname, extraHosts }) { const collection = new Set(); - if ( Array.isArray( original ) ) { - original.forEach( collection.add, collection ); - } else if ( typeof original === 'string' ) { - collection.add( original ); + if (Array.isArray(original)) { + original.forEach(collection.add, collection); + } else if (typeof original === 'string') { + collection.add(original); } - if ( hostname ) { - collection.add( hostname ); + if (hostname) { + collection.add(hostname); } - if ( Array.isArray( extraHosts ) ) { - extraHosts.forEach( collection.add, collection ); + if (Array.isArray(extraHosts)) { + extraHosts.forEach(collection.add, collection); } - const domains = Array.from( collection ); + const domains = Array.from(collection); return domains.length === 1 ? domains[0] : domains; } -function marshalWordPress( original, answers ) { +function marshalWordPress(original, answers) { let wp = original || answers.wordpress; - if ( wp === true ) { + if (wp === true) { wp = {}; } - [ 'title', 'username', 'password', 'email' ].forEach( ( key ) => { - if ( answers[key] ) { + ['title', 'username', 'password', 'email'].forEach((key) => { + if (answers[key]) { wp[key] = answers[key]; } - } ); + }); - if ( answers.wordpressType ) { + if (answers.wordpressType) { wp.type = answers.wordpressType; } - if ( answers.emptyContent ) { + if (answers.emptyContent) { wp.purify = true; } return wp; } -module.exports = function makeInquirer( { prompt } ) { - return async ( defaults = {} ) => { - const { - name, - domain, - mediaProxy, - php, - elasticsearch, - wordpress, - } = defaults; +module.exports = function makeInquirer({ prompt }) { + return async (defaults = {}) => { + const { name, domain, mediaProxy, php, elasticsearch, wordpress } = defaults; const { type: wordpressType, @@ -89,7 +71,7 @@ module.exports = function makeInquirer( { prompt } ) { purify: wordpressPurify, } = wordpress || {}; - const answers = await prompt( [ + const answers = await prompt([ { name: 'hostname', type: 'input', @@ -97,7 +79,7 @@ module.exports = function makeInquirer( { prompt } ) { validate: validateNotEmpty, filter: parseHostname, when() { - return !domain || ( Array.isArray( domain ) && !domain.length ); + return !domain || (Array.isArray(domain) && !domain.length); }, }, { @@ -106,21 +88,22 @@ module.exports = function makeInquirer( { prompt } ) { message: 'Are there additional domains the site should respond to?', default: false, when() { - return !domain || ( Array.isArray( domain ) && !domain.length ); + return !domain || (Array.isArray(domain) && !domain.length); }, }, { name: 'extraHosts', type: 'input', - message: 'Enter additional hostnames separated by spaces (Ex: docker1.test docker2.test)', - filter( value ) { + message: + 'Enter additional hostnames separated by spaces (Ex: docker1.test docker2.test)', + filter(value) { return value - .split( ' ' ) - .map( ( value ) => value.trim() ) - .filter( ( value ) => value.length > 0 ) - .map( parseHostname ); + .split(' ') + .map((value) => value.trim()) + .filter((value) => value.length > 0) + .map(parseHostname); }, - when( answers ) { + when(answers) { return answers.addMoreHosts === true; }, }, @@ -131,14 +114,14 @@ module.exports = function makeInquirer( { prompt } ) { choices: phpVersions, default: '7.4', when() { - return !phpVersions.includes( php ); + return !phpVersions.includes(php); }, }, { name: 'wordpress', type: 'confirm', message: 'Do you want to install WordPress?', - when: defaultIsUndefined( wordpress ), + when: defaultIsUndefined(wordpress), }, { name: 'wordpressType', @@ -146,9 +129,11 @@ module.exports = function makeInquirer( { prompt } ) { message: 'Select a WordPress installation type:', choices: wordpressTypes, default: 'single', - when( answers ) { + when(answers) { const installWp = answers.wordpress === true; - const wrongType = wordpressType && !wordpressTypes.map( ( { value } ) => value ).includes( wordpressType ); + const wrongType = + wordpressType && + !wordpressTypes.map(({ value }) => value).includes(wordpressType); return installWp || wrongType; }, }, @@ -156,12 +141,12 @@ module.exports = function makeInquirer( { prompt } ) { name: 'title', type: 'input', message: 'Site Name', - default( { hostname } ) { + default({ hostname }) { return hostname; }, validate: validateNotEmpty, - when( answers ) { - return answers.wordpress === true || ( wordpress && !wordpressTitle ); + when(answers) { + return answers.wordpress === true || (wordpress && !wordpressTitle); }, }, { @@ -170,8 +155,8 @@ module.exports = function makeInquirer( { prompt } ) { message: 'Admin Username', default: 'admin', validate: validateNotEmpty, - when( answers ) { - return answers.wordpress === true || ( wordpress && !wordpressUsername ); + when(answers) { + return answers.wordpress === true || (wordpress && !wordpressUsername); }, }, { @@ -180,8 +165,8 @@ module.exports = function makeInquirer( { prompt } ) { message: 'Admin Password', default: 'password', validate: validateNotEmpty, - when( answers ) { - return answers.wordpress === true || ( wordpress && !wordpressPassword ); + when(answers) { + return answers.wordpress === true || (wordpress && !wordpressPassword); }, }, { @@ -190,8 +175,8 @@ module.exports = function makeInquirer( { prompt } ) { message: 'Admin Email', default: 'admin@example.com', validate: validateNotEmpty, - when( answers ) { - return answers.wordpress === true || ( wordpress && !wordpressEmail ); + when(answers) { + return answers.wordpress === true || (wordpress && !wordpressEmail); }, }, { @@ -199,27 +184,28 @@ module.exports = function makeInquirer( { prompt } ) { type: 'confirm', message: 'Do you want to remove the default content?', default: false, - when( answers ) { - return answers.wordpress === true || ( wordpress && !wordpressPurify ); + when(answers) { + return answers.wordpress === true || (wordpress && !wordpressPurify); }, }, { name: 'mediaProxy', type: 'confirm', - message: 'Do you want to set a proxy for media assets? (i.e. Serving /uploads/ directory assets from a production site)', + message: + 'Do you want to set a proxy for media assets? (i.e. Serving /uploads/ directory assets from a production site)', default: false, - when: defaultIsUndefined( mediaProxy ), + when: defaultIsUndefined(mediaProxy), }, { name: 'proxy', type: 'input', message: 'Proxy URL', - default( { hostname } ) { - return createDefaultProxy( hostname ); + default({ hostname }) { + return createDefaultProxy(hostname); }, validate: validateNotEmpty, filter: parseProxyUrl, - when( answers ) { + when(answers) { return answers.mediaProxy === true; }, }, @@ -228,18 +214,18 @@ module.exports = function makeInquirer( { prompt } ) { type: 'confirm', message: 'Do you need Elasticsearch', default: false, - when: defaultIsUndefined( elasticsearch ), + when: defaultIsUndefined(elasticsearch), }, - ] ); + ]); return { ...defaults, name: name || answers.title, - domain: marshalDomains( domain, answers ), + domain: marshalDomains(domain, answers), mediaProxy: answers.proxy || false, php: php || answers.phpVersion, elasticsearch: elasticsearch || answers.elasticsearch || false, - wordpress: marshalWordPress( wordpress, answers ), + wordpress: marshalWordPress(wordpress, answers), }; }; }; diff --git a/src/commands/create/install-wordpress.js b/src/commands/create/install-wordpress.js index 13aeb67e..20802014 100644 --- a/src/commands/create/install-wordpress.js +++ b/src/commands/create/install-wordpress.js @@ -1,149 +1,160 @@ -const envUtils = require( '../../env-utils' ); -const compose = require( '../../utils/docker-compose' ); +const envUtils = require('../../env-utils'); +const compose = require('../../utils/docker-compose'); -async function downloadWordPress( wordpressType, cwd, spinner ) { - if ( spinner ) { - spinner.start( 'Downloading WordPress...' ); +async function downloadWordPress(wordpressType, cwd, spinner) { + if (spinner) { + spinner.start('Downloading WordPress...'); } else { - console.log( 'Downloading WordPress:' ); + console.log('Downloading WordPress:'); } - await compose.exec( 'phpfpm', 'wp core download --version=latest --force', { cwd, log: ! spinner } ); + await compose.exec('phpfpm', 'wp core download --version=latest --force', { + cwd, + log: !spinner, + }); - if ( spinner ) { - spinner.succeed( 'WordPress is downloaded...' ); + if (spinner) { + spinner.succeed('WordPress is downloaded...'); } else { - console.log( ' - Done' ); + console.log(' - Done'); } } -async function configure( envSlug, cwd, spinner ) { - const command = `wp config create --force --dbname=${ envSlug } --dbuser=wordpress --dbpass=password --dbhost=mysql`; +async function configure(envSlug, cwd, spinner) { + const command = `wp config create --force --dbname=${envSlug} --dbuser=wordpress --dbpass=password --dbhost=mysql`; - if ( spinner ) { - spinner.start( 'Configuring WordPress...' ); + if (spinner) { + spinner.start('Configuring WordPress...'); } else { - console.log( 'Create WordPress config:' ); + console.log('Create WordPress config:'); } - await compose.exec( 'phpfpm', command, { cwd, log: ! spinner } ); + await compose.exec('phpfpm', command, { cwd, log: !spinner }); - if ( spinner ) { - spinner.succeed( 'WordPress config is created...' ); + if (spinner) { + spinner.succeed('WordPress config is created...'); } else { - console.log( ' - Done' ); + console.log(' - Done'); } } -async function install( hostname, wordpress, certs, cwd, spinner ) { - const { - title, - username, - password, - email, - type, - } = wordpress; - - const command = [ 'wp', 'core' ]; - switch ( type ) { +async function install(hostname, wordpress, certs, cwd, spinner) { + const { title, username, password, email, type } = wordpress; + + const command = ['wp', 'core']; + switch (type) { case 'single': case 'dev': - command.push( 'install' ); + command.push('install'); break; case 'subdirectory': - command.push( 'multisite-install' ); + command.push('multisite-install'); break; case 'subdomain': - command.push( 'multisite-install' ); - command.push( '--subdomains' ); + command.push('multisite-install'); + command.push('--subdomains'); break; default: - throw Error( 'Invalid Installation Type' ); + throw Error('Invalid Installation Type'); } const http = certs ? 'https' : 'http'; - command.push( `--url=${ http }://${ hostname }` ); - command.push( `--title=${ title }` ); - command.push( `--admin_user=${ username }` ); - command.push( `--admin_password=${ password }` ); - command.push( `--admin_email=${ email }` ); + command.push(`--url=${http}://${hostname}`); + command.push(`--title=${title}`); + command.push(`--admin_user=${username}`); + command.push(`--admin_password=${password}`); + command.push(`--admin_email=${email}`); - if ( spinner ) { - spinner.start( 'Installing WordPress...' ); + if (spinner) { + spinner.start('Installing WordPress...'); } else { - console.log( 'Install WordPress:' ); + console.log('Install WordPress:'); } - await compose.exec( 'phpfpm', command, { cwd, log: ! spinner } ); + await compose.exec('phpfpm', command, { cwd, log: !spinner }); - if ( spinner ) { - spinner.succeed( 'WordPress is installed...' ); + if (spinner) { + spinner.succeed('WordPress is installed...'); } else { - console.log( ' - Done' ); + console.log(' - Done'); } } -async function setRewrites( cwd, spinner ) { - if ( spinner ) { - spinner.start( 'Setting rewrite rules structure to /%postname%/...' ); +async function setRewrites(cwd, spinner) { + if (spinner) { + spinner.start('Setting rewrite rules structure to /%postname%/...'); } else { - console.log( 'Update rewrite rules:' ); + console.log('Update rewrite rules:'); } - await compose.exec( 'phpfpm', 'wp rewrite structure /%postname%/', { cwd, log: ! spinner } ); + await compose.exec('phpfpm', 'wp rewrite structure /%postname%/', { cwd, log: !spinner }); - if ( spinner ) { - spinner.succeed( 'Rewrite rules structure is updated to /%postname%/...' ); + if (spinner) { + spinner.succeed('Rewrite rules structure is updated to /%postname%/...'); } else { - console.log( ' - Done' ); + console.log(' - Done'); } } -async function emptyContent( cwd, spinner ) { - if ( spinner ) { - spinner.start( 'Removing the default WordPress content...' ); +async function emptyContent(cwd, spinner) { + if (spinner) { + spinner.start('Removing the default WordPress content...'); } else { - console.log( 'Remove the default WordPress content:' ); + console.log('Remove the default WordPress content:'); } - await compose.exec( 'phpfpm', 'wp site empty --yes', { cwd, log: ! spinner } ).catch( () => {} ); - await compose.exec( 'phpfpm', 'wp plugin delete hello akismet', { cwd, log: ! spinner } ).catch( () => {} ); - await compose.exec( 'phpfpm', 'wp theme delete twentyfifteen twentysixteen twentyseventeen twentyeighteen twentynineteen', { cwd, log: ! spinner } ).catch( () => {} ); - await compose.exec( 'phpfpm', 'wp widget delete search-2 recent-posts-2 recent-comments-2 archives-2 categories-2 meta-2', { cwd, log: ! spinner } ).catch( () => {} ); - - if ( spinner ) { - spinner.succeed( 'The default content is removed...' ); + await compose.exec('phpfpm', 'wp site empty --yes', { cwd, log: !spinner }).catch(() => {}); + await compose + .exec('phpfpm', 'wp plugin delete hello akismet', { cwd, log: !spinner }) + .catch(() => {}); + await compose + .exec( + 'phpfpm', + 'wp theme delete twentyfifteen twentysixteen twentyseventeen twentyeighteen twentynineteen', + { cwd, log: !spinner }, + ) + .catch(() => {}); + await compose + .exec( + 'phpfpm', + 'wp widget delete search-2 recent-posts-2 recent-comments-2 archives-2 categories-2 meta-2', + { cwd, log: !spinner }, + ) + .catch(() => {}); + + if (spinner) { + spinner.succeed('The default content is removed...'); } else { - console.log( ' - Done' ); + console.log(' - Done'); } } -module.exports = function makeInstallWordPress( spinner ) { - return async ( hostname, settings ) => { +module.exports = function makeInstallWordPress(spinner) { + return async (hostname, settings) => { const { wordpress, certs, envSlug } = settings; - if ( ! wordpress ) { + if (!wordpress) { return; } try { - const cwd = await envUtils.envPath( envSlug ); + const cwd = await envUtils.envPath(envSlug); - await downloadWordPress( wordpress.type, cwd, spinner ); - await configure( envSlug, cwd, spinner ); - await install( hostname, wordpress, certs, cwd, spinner ); - await setRewrites( cwd, spinner ); + await downloadWordPress(wordpress.type, cwd, spinner); + await configure(envSlug, cwd, spinner); + await install(hostname, wordpress, certs, cwd, spinner); + await setRewrites(cwd, spinner); - if ( wordpress.purify ) { - await emptyContent( cwd, spinner ); + if (wordpress.purify) { + await emptyContent(cwd, spinner); } - } catch( error ) { - if ( spinner ) { + } catch (error) { + if (spinner) { spinner.stop(); } - if ( error.err ) { - throw new Error( error.err ); + if (error.err) { + throw new Error(error.err); } else { throw error; } diff --git a/src/commands/create/make-cert.js b/src/commands/create/make-cert.js index 6661c705..84e4cd9a 100644 --- a/src/commands/create/make-cert.js +++ b/src/commands/create/make-cert.js @@ -1,30 +1,34 @@ -const { generate } = require( '../../certificates' ); +const { generate } = require('../../certificates'); -module.exports = function makeCert( spinner ) { - return async ( envSlug, hosts ) => { - if ( ! spinner ) { - console.log( 'Generating certificates:' ); +module.exports = function makeCert(spinner) { + return async (envSlug, hosts) => { + if (!spinner) { + console.log('Generating certificates:'); } - const certs = await generate( envSlug, hosts ).catch( ( err ) => { - if ( err && err.message ) { - if ( spinner ) { - spinner.warn( err.message ); - spinner.info( 'Failed to create SSL certificates, HTTP version will be created...' ); + const certs = await generate(envSlug, hosts).catch((err) => { + if (err && err.message) { + if (spinner) { + spinner.warn(err.message); + spinner.info( + 'Failed to create SSL certificates, HTTP version will be created...', + ); } else { - console.error( err.message ); - console.log( 'Failed to create SSL certificates, HTTP version will be created...' ); + console.error(err.message); + console.log( + 'Failed to create SSL certificates, HTTP version will be created...', + ); } } return false; - } ); + }); - if ( certs ) { - if ( spinner ) { - spinner.succeed( 'Certificates are generated...' ); + if (certs) { + if (spinner) { + spinner.succeed('Certificates are generated...'); } else { - console.log( ' - Done' ); + console.log(' - Done'); } } diff --git a/src/commands/create/make-docker-compose.js b/src/commands/create/make-docker-compose.js index 632f096a..089647e6 100644 --- a/src/commands/create/make-docker-compose.js +++ b/src/commands/create/make-docker-compose.js @@ -1,31 +1,25 @@ -const { platform } = require( 'os' ); +const { platform } = require('os'); -const slugify = require( '@sindresorhus/slugify' ); +const slugify = require('@sindresorhus/slugify'); -const { cacheVolume } = require( '../../env-utils' ); -const { images } = require( '../../docker-images' ); -const config = require( '../../configure' ); +const { cacheVolume } = require('../../env-utils'); +const { images } = require('../../docker-images'); +const config = require('../../configure'); -module.exports = function makeDockerCompose( spinner ) { - return async ( hosts, settings ) => { - if ( spinner ) { - spinner.start( 'Creating docker-compose configuration...' ); +module.exports = function makeDockerCompose(spinner) { + return async (hosts, settings) => { + if (spinner) { + spinner.start('Creating docker-compose configuration...'); } else { - console.log( 'Create docker-compose configuration:' ); + console.log('Create docker-compose configuration:'); } - const { - envSlug, - php: phpVersion, - wordpress, - elasticsearch, - certs, - } = settings; + const { envSlug, php: phpVersion, wordpress, elasticsearch, certs } = settings; const { type: wordpressType } = wordpress || {}; - const allHosts = [ ...hosts, ...hosts.map( ( host ) => `*.${ host }` ) ]; + const allHosts = [...hosts, ...hosts.map((host) => `*.${host}`)]; - const wpsnapshotsDir = await config.get( 'snapshotsPath' ); + const wpsnapshotsDir = await config.get('snapshotsPath'); const baseConfig = { // use version 2 so we can use limits @@ -36,9 +30,9 @@ module.exports = function makeDockerCompose( spinner ) { }, nginx: { image: images['nginx'], - expose: [ '80', '443' ], - depends_on: [ 'phpfpm' ], - networks: [ 'default', 'wplocaldocker' ], + expose: ['80', '443'], + depends_on: ['phpfpm'], + networks: ['default', 'wplocaldocker'], volumes: [ './wordpress:/var/www/html:cached', './config/nginx/server.conf:/etc/nginx/conf.d/common/_server.conf:cached', @@ -46,25 +40,23 @@ module.exports = function makeDockerCompose( spinner ) { environment: { CERT_NAME: certs ? envSlug : 'localhost', HTTPS_METHOD: 'noredirect', - VIRTUAL_HOST: allHosts.join( ',' ), + VIRTUAL_HOST: allHosts.join(','), }, }, phpfpm: { - image: images[`php${ phpVersion }`], - depends_on: [ 'memcached' ], - networks: [ 'default', 'wplocaldocker' ], - dns: [ '10.0.0.2' ], + image: images[`php${phpVersion}`], + depends_on: ['memcached'], + networks: ['default', 'wplocaldocker'], + dns: ['10.0.0.2'], volumes: [ './wordpress:/var/www/html:cached', - `./config/php-fpm/docker-php-ext-xdebug.ini:/etc/php/${ phpVersion }/fpm/conf.d/docker-php-ext-xdebug.ini:cached`, - `${ cacheVolume }:/var/www/.wp-cli/cache:cached`, - ], - cap_add: [ - 'SYS_PTRACE', + `./config/php-fpm/docker-php-ext-xdebug.ini:/etc/php/${phpVersion}/fpm/conf.d/docker-php-ext-xdebug.ini:cached`, + `${cacheVolume}:/var/www/.wp-cli/cache:cached`, ], + cap_add: ['SYS_PTRACE'], environment: { ENABLE_XDEBUG: 'true', - PHP_IDE_CONFIG: `serverName=${ envSlug }`, + PHP_IDE_CONFIG: `serverName=${envSlug}`, }, }, }, @@ -75,7 +67,7 @@ module.exports = function makeDockerCompose( spinner ) { }, }, volumes: { - [ cacheVolume ]: { + [cacheVolume]: { name: cacheVolume, external: true, }, @@ -87,44 +79,56 @@ module.exports = function makeDockerCompose( spinner ) { // file system. Because of this the phpfpm container will be running as the // wrong user. Here we setup the docker-compose.yml file to rebuild the // phpfpm container so that it runs as the user who created the project. - if ( platform() == 'linux' ) { - baseConfig.services.phpfpm.image = `wp-php-fpm-dev-${ phpVersion }-${ slugify( process.env.USER ) }`; - baseConfig.services.phpfpm.volumes.push( `~/.ssh:/home/${ process.env.USER }/.ssh:cached` ); - baseConfig.services.phpfpm.volumes.push( `${ wpsnapshotsDir }:/home/${ process.env.USER }/.wpsnapshots:cached` ); - baseConfig.services.phpfpm.volumes.push( `~/.aws:/home/${ process.env.USER }/.aws:cached,ro` ); + if (platform() == 'linux') { + baseConfig.services.phpfpm.image = `wp-php-fpm-dev-${phpVersion}-${slugify(process.env.USER)}`; + baseConfig.services.phpfpm.volumes.push(`~/.ssh:/home/${process.env.USER}/.ssh:cached`); + baseConfig.services.phpfpm.volumes.push( + `${wpsnapshotsDir}:/home/${process.env.USER}/.wpsnapshots:cached`, + ); + baseConfig.services.phpfpm.volumes.push( + `~/.aws:/home/${process.env.USER}/.aws:cached,ro`, + ); baseConfig.services.phpfpm.build = { dockerfile: 'php-fpm', context: '.containers', args: { - PHP_IMAGE: images[`php${ phpVersion }`], + PHP_IMAGE: images[`php${phpVersion}`], CALLING_USER: process.env.USER, - CALLING_UID: process.getuid() - } + CALLING_UID: process.getuid(), + }, }; } else { // the official containers for this project will have a www-data user. - baseConfig.services.phpfpm.volumes.push( '~/.ssh:/home/www-data/.ssh:cached' ); - baseConfig.services.phpfpm.volumes.push( `${ wpsnapshotsDir }:/home/www-data/.wpsnapshots:cached` ); - baseConfig.services.phpfpm.volumes.push( '~/.aws:/home/www-data/.aws:cached,ro' ); + baseConfig.services.phpfpm.volumes.push('~/.ssh:/home/www-data/.ssh:cached'); + baseConfig.services.phpfpm.volumes.push( + `${wpsnapshotsDir}:/home/www-data/.wpsnapshots:cached`, + ); + baseConfig.services.phpfpm.volumes.push('~/.aws:/home/www-data/.aws:cached,ro'); } let nginxConfig = 'default.conf'; - if ( wordpressType == 'dev' ) { + if (wordpressType == 'dev') { nginxConfig = 'develop.conf'; - baseConfig.services.phpfpm.volumes.push( './config/php-fpm/wp-cli.develop.yml:/var/www/.wp-cli/config.yml:cached' ); + baseConfig.services.phpfpm.volumes.push( + './config/php-fpm/wp-cli.develop.yml:/var/www/.wp-cli/config.yml:cached', + ); } else { - baseConfig.services.phpfpm.volumes.push( './config/php-fpm/wp-cli.local.yml:/var/www/.wp-cli/config.yml:cached' ); + baseConfig.services.phpfpm.volumes.push( + './config/php-fpm/wp-cli.local.yml:/var/www/.wp-cli/config.yml:cached', + ); } // Map the nginx configuraiton file - baseConfig.services.nginx.volumes.push( `./config/nginx/${ nginxConfig }:/etc/nginx/conf.d/default.conf:cached` ); + baseConfig.services.nginx.volumes.push( + `./config/nginx/${nginxConfig}:/etc/nginx/conf.d/default.conf:cached`, + ); - if ( elasticsearch ) { - baseConfig.services.phpfpm.depends_on.push( 'elasticsearch' ); + if (elasticsearch) { + baseConfig.services.phpfpm.depends_on.push('elasticsearch'); baseConfig.services.elasticsearch = { image: images['elasticsearch'], - expose: [ '9200' ], + expose: ['9200'], mem_limit: '1024M', mem_reservation: '1024M', volumes: [ @@ -142,17 +146,19 @@ module.exports = function makeDockerCompose( spinner ) { } let dockerComposeConfig = { ...baseConfig }; - if ( typeof settings.dockerCompose === 'function' ) { - const filteredConfig = await settings.dockerCompose.call( settings, baseConfig, { spinner } ); - if ( filteredConfig ) { + if (typeof settings.dockerCompose === 'function') { + const filteredConfig = await settings.dockerCompose.call(settings, baseConfig, { + spinner, + }); + if (filteredConfig) { dockerComposeConfig = { ...filteredConfig }; } } - if ( spinner ) { - spinner.succeed( 'Docker-compose configuration is created...' ); + if (spinner) { + spinner.succeed('Docker-compose configuration is created...'); } else { - console.log( ' - Done' ); + console.log(' - Done'); } return dockerComposeConfig; diff --git a/src/commands/create/make-fs.js b/src/commands/create/make-fs.js index aceed1f9..b41ea24a 100644 --- a/src/commands/create/make-fs.js +++ b/src/commands/create/make-fs.js @@ -1,36 +1,38 @@ -const { join } = require( 'path' ); -const { stat, mkdir } = require( 'fs' ).promises; +const { join } = require('path'); +const { stat, mkdir } = require('fs').promises; -const envUtils = require( '../../env-utils' ); +const envUtils = require('../../env-utils'); -module.exports = function makeFs( spinner ) { - return async ( hostname ) => { - const envPath = await envUtils.envPath( hostname ); - const envPathStats = await stat( envPath ).catch( () => false ); +module.exports = function makeFs(spinner) { + return async (hostname) => { + const envPath = await envUtils.envPath(hostname); + const envPathStats = await stat(envPath).catch(() => false); // @ts-ignore - if ( envPathStats && envPathStats.isDirectory() ) { - throw new Error( `Error: ${ hostname } environment already exists. To recreate the environment, please delete it first by running \`wpdocker delete ${ hostname }\`` ); + if (envPathStats && envPathStats.isDirectory()) { + throw new Error( + `Error: ${hostname} environment already exists. To recreate the environment, please delete it first by running \`wpdocker delete ${hostname}\``, + ); } - const wordpress = join( envPath, 'wordpress' ); - const containers = join( envPath, '.containers' ); - const config = join( envPath, 'config' ); + const wordpress = join(envPath, 'wordpress'); + const containers = join(envPath, '.containers'); + const config = join(envPath, 'config'); const options = { mode: 0o755, recursive: true, }; - await mkdir( envPath, options ); - await mkdir( wordpress, options ); - await mkdir( containers, options ); - await mkdir( config, options ); + await mkdir(envPath, options); + await mkdir(wordpress, options); + await mkdir(containers, options); + await mkdir(config, options); - if ( spinner ) { - spinner.succeed( 'Environment directories have been created...' ); + if (spinner) { + spinner.succeed('Environment directories have been created...'); } else { - console.log( 'Environment directories have been created.' ); + console.log('Environment directories have been created.'); } return { diff --git a/src/commands/create/save-json-file.js b/src/commands/create/save-json-file.js index ddc114f4..8b4d9d89 100644 --- a/src/commands/create/save-json-file.js +++ b/src/commands/create/save-json-file.js @@ -1,8 +1,8 @@ -const { join } = require( 'path' ); -const { writeFile } = require( 'fs' ).promises; +const { join } = require('path'); +const { writeFile } = require('fs').promises; -module.exports = function makeJsonFile( root ) { - return ( filename, data ) => { - return writeFile( join( root, filename ), JSON.stringify( data ) ); +module.exports = function makeJsonFile(root) { + return (filename, data) => { + return writeFile(join(root, filename), JSON.stringify(data)); }; }; diff --git a/src/commands/create/save-yaml-file.js b/src/commands/create/save-yaml-file.js index d0a73baa..0e368ddb 100644 --- a/src/commands/create/save-yaml-file.js +++ b/src/commands/create/save-yaml-file.js @@ -1,15 +1,16 @@ -const { join } = require( 'path' ); +const { join } = require('path'); -const yaml = require( 'write-yaml' ); +const yaml = require('write-yaml'); -module.exports = function makeSaveYamlFile( root ) { - return ( filename, data ) => new Promise( ( resolve, reject ) => { - yaml( join( root, filename ), data, { lineWidth: 500 }, ( err ) => { - if ( err ) { - reject( err ); - } else { - resolve(); - } - } ); - } ); +module.exports = function makeSaveYamlFile(root) { + return (filename, data) => + new Promise((resolve, reject) => { + yaml(join(root, filename), data, { lineWidth: 500 }, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); }; diff --git a/src/commands/create/update-hosts.js b/src/commands/create/update-hosts.js index e7ffa919..94502859 100644 --- a/src/commands/create/update-hosts.js +++ b/src/commands/create/update-hosts.js @@ -1,34 +1,38 @@ -const { resolve, join } = require( 'path' ); +const { resolve, join } = require('path'); -const config = require( '../../configure' ); +const config = require('../../configure'); -module.exports = function makeUpdateHosts( which, sudo, spinner ) { - return async ( hosts ) => { - const manageHosts = await config.get( 'manageHosts' ); - if ( manageHosts !== true ) { +module.exports = function makeUpdateHosts(which, sudo, spinner) { + return async (hosts) => { + const manageHosts = await config.get('manageHosts'); + if (manageHosts !== true) { return; } - const node = await which( 'node' ); - const hostsScript = join( resolve( __dirname, '../../..' ), 'hosts.js' ); + const node = await which('node'); + const hostsScript = join(resolve(__dirname, '../../..'), 'hosts.js'); - await new Promise( ( resolve ) => { - const command = `${ node } "${ hostsScript }" add ${ hosts.join( ' ' ) }`; - sudo.exec( command, { name: 'WP Docker' }, ( err ) => { - if ( err ) { - if ( spinner ) { - spinner.warn( 'Something went wrong adding host file entries. You may need to add the /etc/hosts entries manually.' ); + await new Promise((resolve) => { + const command = `${node} "${hostsScript}" add ${hosts.join(' ')}`; + sudo.exec(command, { name: 'WP Docker' }, (err) => { + if (err) { + if (spinner) { + spinner.warn( + 'Something went wrong adding host file entries. You may need to add the /etc/hosts entries manually.', + ); } else { - console.log( 'Something went wrong adding host file entries. You may need to add the /etc/hosts entries manually.' ); + console.log( + 'Something went wrong adding host file entries. You may need to add the /etc/hosts entries manually.', + ); } } else { - if ( spinner ) { - spinner.succeed( 'Added domains to the hosts file...' ); + if (spinner) { + spinner.succeed('Added domains to the hosts file...'); } } resolve(); - } ); - } ); + }); + }); }; }; diff --git a/src/commands/delete.js b/src/commands/delete.js index eab2880b..e66c6267 100644 --- a/src/commands/delete.js +++ b/src/commands/delete.js @@ -1,27 +1,27 @@ -const makeCommand = require( '../utils/make-command' ); -const makeSpinner = require( '../utils/make-spinner' ); -const { resolveEnvironment } = require( '../env-utils' ); -const { deleteAll, deleteEnv } = require( '../environment' ); +const makeCommand = require('../utils/make-command'); +const makeSpinner = require('../utils/make-spinner'); +const { resolveEnvironment } = require('../env-utils'); +const { deleteAll, deleteEnv } = require('../environment'); exports.command = 'delete []'; exports.desc = 'Deletes a specific environment.'; -exports.aliases = [ 'remove', 'rm' ]; +exports.aliases = ['remove', 'rm']; -exports.builder = function( yargs ) { - yargs.positional( 'env', { +exports.builder = function (yargs) { + yargs.positional('env', { type: 'string', describe: 'Optional. Environment name.', - } ); + }); }; -exports.handler = makeCommand( async ( { verbose, env } ) => { - const spinner = ! verbose ? makeSpinner() : undefined; +exports.handler = makeCommand(async ({ verbose, env }) => { + const spinner = !verbose ? makeSpinner() : undefined; const all = env === 'all'; - if ( all ) { - await deleteAll( spinner ); + if (all) { + await deleteAll(spinner); } else { - const envName = await resolveEnvironment( env || '' ); - await deleteEnv( envName, spinner ); + const envName = await resolveEnvironment(env || ''); + await deleteEnv(envName, spinner); } -} ); +}); diff --git a/src/commands/image.js b/src/commands/image.js index e3fee5ce..868f99d1 100644 --- a/src/commands/image.js +++ b/src/commands/image.js @@ -1,6 +1,6 @@ exports.command = 'image '; exports.desc = 'Manages docker images used by this environment.'; -exports.builder = ( yargs ) => { - yargs.commandDir( 'image' ); +exports.builder = (yargs) => { + yargs.commandDir('image'); }; diff --git a/src/commands/image/update.js b/src/commands/image/update.js index 129522e7..9f4568a3 100644 --- a/src/commands/image/update.js +++ b/src/commands/image/update.js @@ -1,132 +1,140 @@ -const os = require( 'os' ); +const os = require('os'); -const inquirer = require( 'inquirer' ); -const chalk = require( 'chalk' ); +const inquirer = require('inquirer'); +const chalk = require('chalk'); -const { stopAll } = require( '../../environment' ); -const promptValidators = require( '../../prompt-validators' ); -const { globalImages, images } = require( '../../docker-images' ); -const makeCommand = require( '../../utils/make-command' ); -const makeSpinner = require( '../../utils/make-spinner' ); -const makeDocker = require( '../../utils/make-docker' ); +const { stopAll } = require('../../environment'); +const promptValidators = require('../../prompt-validators'); +const { globalImages, images } = require('../../docker-images'); +const makeCommand = require('../../utils/make-command'); +const makeSpinner = require('../../utils/make-spinner'); +const makeDocker = require('../../utils/make-docker'); exports.command = 'update [--yes] [--remove-built-images]'; -exports.desc = 'Updates any docker images used by your environment to the latest versions available for the specified tag. All environments must be stopped to update images.'; +exports.desc = + 'Updates any docker images used by your environment to the latest versions available for the specified tag. All environments must be stopped to update images.'; -exports.builder = function( yargs ) { - yargs.positional( 'yes', { +exports.builder = function (yargs) { + yargs.positional('yes', { default: undefined, type: 'boolean', describe: 'Optional. Answer "yes" on all questions.', - } ); + }); - yargs.positional( 'remove-built-images', { + yargs.positional('remove-built-images', { default: false, type: 'boolean', describe: 'Optional. Force removal of custom images built for phpfpm.', - } ); + }); }; -async function removeBuiltImages( docker, spinner ) { - if ( spinner ) { - spinner.start( 'Checking previously built images...' ); +async function removeBuiltImages(docker, spinner) { + if (spinner) { + spinner.start('Checking previously built images...'); } else { - console.log( 'Removing previously built images so they can be built again' ); + console.log('Removing previously built images so they can be built again'); } - const images = await docker.listImages( { filters: '{"label": ["com.10up.wp-local-docker=user-image"]}' } ).catch( () => false ); - if ( Array.isArray( images ) && images.length > 0 ) { - for ( let i = 0; i < images.length; i++ ) { - const name = images[ i ].RepoTags[0]; + const images = await docker + .listImages({ filters: '{"label": ["com.10up.wp-local-docker=user-image"]}' }) + .catch(() => false); + if (Array.isArray(images) && images.length > 0) { + for (let i = 0; i < images.length; i++) { + const name = images[i].RepoTags[0]; - if ( spinner ) { - spinner.start( `Removing ${ chalk.cyan( name ) } image...` ); + if (spinner) { + spinner.start(`Removing ${chalk.cyan(name)} image...`); } - const image = docker.getImage( name ); + const image = docker.getImage(name); await image.remove(); - if ( spinner ) { - spinner.succeed( `${ chalk.cyan( name ) } image has been removed...` ); + if (spinner) { + spinner.succeed(`${chalk.cyan(name)} image has been removed...`); } } - } else if ( spinner ) { - spinner.info( 'No previously built images found. Skipping removal...' ); + } else if (spinner) { + spinner.info('No previously built images found. Skipping removal...'); } } -async function updateIfUsed( docker, name, spinner ) { - if ( spinner ) { - spinner.start( `Checking ${ chalk.cyan( name ) } image...` ); +async function updateIfUsed(docker, name, spinner) { + if (spinner) { + spinner.start(`Checking ${chalk.cyan(name)} image...`); } else { - console.log( `Testing ${ name }` ); + console.log(`Testing ${name}`); } - const image = docker.getImage( name ); - const data = await image.inspect().catch( () => false ); + const image = docker.getImage(name); + const data = await image.inspect().catch(() => false); - if ( !data ) { - if ( spinner ) { - spinner.info( `${ chalk.cyan( name ) } doesn't exist on this system. Skipping update...` ); + if (!data) { + if (spinner) { + spinner.info(`${chalk.cyan(name)} doesn't exist on this system. Skipping update...`); } else { - console.log( `${ name } doesn't exist on this system. Skipping update.` ); + console.log(`${name} doesn't exist on this system. Skipping update.`); } } else { - if ( spinner ) { - spinner.text = `Pulling ${ chalk.cyan( name ) } image...`; + if (spinner) { + spinner.text = `Pulling ${chalk.cyan(name)} image...`; } - const stream = await docker.pull( name ); + const stream = await docker.pull(name); - await new Promise( ( resolve ) => { - docker.modem.followProgress( stream, resolve, ( event ) => { - if ( spinner ) { + await new Promise((resolve) => { + docker.modem.followProgress(stream, resolve, (event) => { + if (spinner) { const { id, status, progressDetail } = event; const { current, total } = progressDetail || {}; - const progress = total ? ` - ${ Math.ceil( ( current || 0 ) * 100 / total ) }%` : ''; + const progress = total + ? ` - ${Math.ceil(((current || 0) * 100) / total)}%` + : ''; - spinner.text = `Pulling ${ chalk.cyan( name ) } image: [${ id }] ${ status }${ progress }...`; + spinner.text = `Pulling ${chalk.cyan(name)} image: [${id}] ${status}${progress}...`; } - } ); - } ); + }); + }); - if ( spinner ) { - spinner.succeed( `${ chalk.cyan( name ) } has been updated...` ); + if (spinner) { + spinner.succeed(`${chalk.cyan(name)} has been updated...`); } } } -exports.handler = makeCommand( {}, async function( { yes, verbose, removeBuiltImages: forceRemoveBuiltImages } ) { - if ( ! yes ) { - const { confirm } = await inquirer.prompt( { - name: 'confirm', - type: 'confirm', - message: 'Updating images requires all environments to be stopped. Is that okay?', - validate: promptValidators.validateNotEmpty, - default: false, - } ); - - if ( ! confirm ) { - return; +exports.handler = makeCommand( + {}, + async function ({ yes, verbose, removeBuiltImages: forceRemoveBuiltImages }) { + if (!yes) { + const { confirm } = await inquirer.prompt({ + name: 'confirm', + type: 'confirm', + message: 'Updating images requires all environments to be stopped. Is that okay?', + validate: promptValidators.validateNotEmpty, + default: false, + }); + + if (!confirm) { + return; + } } - } - const spinner = ! verbose ? makeSpinner() : undefined; - const docker = makeDocker(); + const spinner = !verbose ? makeSpinner() : undefined; + const docker = makeDocker(); - await stopAll( spinner ); + await stopAll(spinner); - const allImages = [ ...Object.values( globalImages ), ...Object.values( images ) ]; - for ( let i = 0; i < allImages.length; i++ ) { - await updateIfUsed( docker, allImages[ i ], spinner ); - } + const allImages = [...Object.values(globalImages), ...Object.values(images)]; + for (let i = 0; i < allImages.length; i++) { + await updateIfUsed(docker, allImages[i], spinner); + } - // delete the built containers on linux so it can be rebuilt with the (possibly) updated phpfpm container - if ( forceRemoveBuiltImages && os.platform() == 'linux' ) { - await removeBuiltImages( docker, spinner ); - } + // delete the built containers on linux so it can be rebuilt with the (possibly) updated phpfpm container + if (forceRemoveBuiltImages && os.platform() == 'linux') { + await removeBuiltImages(docker, spinner); + } - if ( ! spinner ) { - console.log( 'Finished. You can now start your environments again.' ); - } -} ); + if (!spinner) { + console.log('Finished. You can now start your environments again.'); + } + }, +); diff --git a/src/commands/list.js b/src/commands/list.js index 791d3fda..cb22ea05 100644 --- a/src/commands/list.js +++ b/src/commands/list.js @@ -1,55 +1,55 @@ -const chalk = require( 'chalk' ); +const chalk = require('chalk'); -const envUtils = require( '../env-utils' ); -const makeCommand = require( '../utils/make-command' ); -const makeDocker = require( '../utils/make-docker' ); -const { replaceLinks } = require( '../utils/make-link' ); -const makeTable = require( '../utils/make-table' ); +const envUtils = require('../env-utils'); +const makeCommand = require('../utils/make-command'); +const makeDocker = require('../utils/make-docker'); +const { replaceLinks } = require('../utils/make-link'); +const makeTable = require('../utils/make-table'); // Add command name, alias and description. -exports.aliases = [ 'ls' ]; +exports.aliases = ['ls']; exports.command = 'list'; exports.desc = 'Lists all the environments and meta information.'; -exports.handler = makeCommand( async () => { +exports.handler = makeCommand(async () => { // Create docker object and make sure it is available. const docker = makeDocker(); // Get all the environments and initialize a status array. const environments = await envUtils.getAllEnvironments(); - const envStatus = [ [ 'Name', 'Status', 'URL', 'Home' ] ]; + const envStatus = [['Name', 'Status', 'URL', 'Home']]; const links = {}; // Loop through each environment and add details. - for ( const envSlug of environments ) { + for (const envSlug of environments) { // Get path of the current environment to perform dedicated checks. - const envPath = await envUtils.envPath( envSlug ); + const envPath = await envUtils.envPath(envSlug); // Get current environment host name, use the starting index. - const envHosts = await envUtils.getEnvHosts( envPath ); - const certs = await envUtils.getEnvConfig( envPath, 'certs' ); + const envHosts = await envUtils.getEnvHosts(envPath); + const certs = await envUtils.getEnvConfig(envPath, 'certs'); const http = certs ? 'https' : 'http'; - const hostName = `${ http }://${ envHosts[0] }/`; + const hostName = `${http}://${envHosts[0]}/`; - links[ hostName ] = hostName; + links[hostName] = hostName; try { - const containers = await docker.listContainers( { filters: { 'name': [ envSlug ] } } ); + const containers = await docker.listContainers({ filters: { name: [envSlug] } }); // Check containers availability and push to list with appropriate status. - envStatus.push( [ + envStatus.push([ envSlug, - Array.isArray( containers ) && containers.length - ? chalk.bgGreen.whiteBright.bold( ' UP ' ) - : chalk.bgRed.whiteBright.bold( ' DOWN ' ), + Array.isArray(containers) && containers.length + ? chalk.bgGreen.whiteBright.bold(' UP ') + : chalk.bgRed.whiteBright.bold(' DOWN '), hostName, envPath, - ] ); - } catch( ex ) { - console.error( ex ); + ]); + } catch (ex) { + console.error(ex); } } // Output the environment status. - console.log( replaceLinks( makeTable( envStatus ), links ) ); -} ); + console.log(replaceLinks(makeTable(envStatus), links)); +}); diff --git a/src/commands/logs.js b/src/commands/logs.js index d820586e..46084414 100644 --- a/src/commands/logs.js +++ b/src/commands/logs.js @@ -1,83 +1,86 @@ -const { EOL } = require( 'os' ); +const { EOL } = require('os'); -const envUtils = require( '../env-utils' ); -const gateway = require( '../gateway' ); -const environment = require( '../environment' ); -const makeCommand = require( '../utils/make-command' ); -const makeSpinner = require( '../utils/make-spinner' ); -const compose = require( '../utils/docker-compose' ); +const envUtils = require('../env-utils'); +const gateway = require('../gateway'); +const environment = require('../environment'); +const makeCommand = require('../utils/make-command'); +const makeSpinner = require('../utils/make-spinner'); +const compose = require('../utils/docker-compose'); exports.command = 'logs [] [--tail=]'; -exports.desc = 'Shows logs from the specified container in your environment (Defaults to all containers).'; +exports.desc = + 'Shows logs from the specified container in your environment (Defaults to all containers).'; -exports.builder = function( yargs ) { - yargs.positional( 'container', { +exports.builder = function (yargs) { + yargs.positional('container', { describe: 'Container name', type: 'string', - } ); + }); - yargs.option( 'tail', { + yargs.option('tail', { description: 'Number of lines to show from the end of the logs for each container', default: 'all', type: 'string', - } ); + }); }; -exports.handler = makeCommand( async ( { verbose, container, env, tail } ) => { +exports.handler = makeCommand(async ({ verbose, container, env, tail }) => { let envSlug = env; - if ( ! envSlug ) { + if (!envSlug) { envSlug = await envUtils.parseOrPromptEnv(); } - if ( envSlug === false ) { - throw new Error( 'Error: Unable to determine which environment to show logs from. Please run this command from within your environment.' ); + if (envSlug === false) { + throw new Error( + 'Error: Unable to determine which environment to show logs from. Please run this command from within your environment.', + ); } - const envPath = await envUtils.envPath( envSlug ); + const envPath = await envUtils.envPath(envSlug); const services = []; - const spinner = ! verbose ? makeSpinner() : undefined; + const spinner = !verbose ? makeSpinner() : undefined; - if ( container ) { - spinner && spinner.start( 'Checking available services...' ); + if (container) { + spinner && spinner.start('Checking available services...'); - const out = await compose.ps( { - commandOptions: [ '--services' ], + const out = await compose.ps({ + commandOptions: ['--services'], cwd: envPath, - } ); + }); const availableServices = out - .split( EOL ) - .filter( ( service ) => service.trim().length > 0 ) - .map( ( service ) => service.toLowerCase() ); + .split(EOL) + .filter((service) => service.trim().length > 0) + .map((service) => service.toLowerCase()); - if ( availableServices.includes( container.toLowerCase() ) ) { - services.push( container ); - spinner && spinner.succeed( 'Container is available...' ); + if (availableServices.includes(container.toLowerCase())) { + services.push(container); + spinner && spinner.succeed('Container is available...'); } else { - spinner && spinner.warn( 'Container is not found, using all containers...' ); + spinner && spinner.warn('Container is not found, using all containers...'); } } // Check if the container is running, otherwise, start up the stacks - const out = await compose.ps( { cwd: envPath } ); + const out = await compose.ps({ cwd: envPath }); if ( - ( services.length > 0 && out.indexOf( services[0] ) === -1 ) || - out.split( EOL ).filter( ( line ) => line.trim().length > 0 ).length <= 2 + (services.length > 0 && out.indexOf(services[0]) === -1) || + out.split(EOL).filter((line) => line.trim().length > 0).length <= 2 ) { - spinner && spinner.info( 'Environment is not running, starting it...' ); - await gateway.startGlobal( spinner ); - await environment.start( envSlug, spinner ); + spinner && spinner.info('Environment is not running, starting it...'); + await gateway.startGlobal(spinner); + await environment.start(envSlug, spinner); } - let tailCount = tail === 'all' ? tail : parseInt( tail, 10 ); - if ( Number.isNaN( tailCount ) ) { + let tailCount = tail === 'all' ? tail : parseInt(tail, 10); + if (Number.isNaN(tailCount)) { tailCount = 'all'; } - await compose.logs( services, { - commandOptions: [ `--tail=${ tailCount }` ], + await compose.logs(services, { + commandOptions: [`--tail=${tailCount}`], cwd: envPath, follow: true, log: true, - } ); -} ); + }); +}); diff --git a/src/commands/migrate.js b/src/commands/migrate.js index 5cf2bd85..305d9d82 100644 --- a/src/commands/migrate.js +++ b/src/commands/migrate.js @@ -1,144 +1,157 @@ -const path = require( 'path' ); -const { execSync } = require( 'child_process' ); -const { exec } = require( 'child_process' ); +const path = require('path'); +const { execSync } = require('child_process'); +const { exec } = require('child_process'); -const fs = require( 'fs-extra' ); -const chalk = require( 'chalk' ); +const fs = require('fs-extra'); +const chalk = require('chalk'); -const envUtils = require( '../env-utils' ); -const { start } = require( '../environment' ); -const makeCommand = require( '../utils/make-command' ); -const makeSpinner = require( '../utils/make-spinner' ); -const compose = require( '../utils/docker-compose' ); +const envUtils = require('../env-utils'); +const { start } = require('../environment'); +const makeCommand = require('../utils/make-command'); +const makeSpinner = require('../utils/make-spinner'); +const compose = require('../utils/docker-compose'); exports.command = 'migrate []'; -exports.desc = 'Migrates a V1 WP Docker environment to a new V2 environment. Before running this command, create a new environment using the `wpdocker create` command.'; +exports.desc = + 'Migrates a V1 WP Docker environment to a new V2 environment. Before running this command, create a new environment using the `wpdocker create` command.'; -exports.builder = function ( yargs ) { - yargs.positional( 'old', { +exports.builder = function (yargs) { + yargs.positional('old', { type: 'string', describe: 'Path to the old environment.', - } ); + }); - yargs.positional( 'env', { + yargs.positional('env', { type: 'string', describe: 'Optional. Environment to migrate the V1 data into.', - } ); + }); }; // Kind of like the one for gateway, but not using docker compose and adapted for this purpose -function waitForDB( containerName ) { +function waitForDB(containerName) { const readyMatch = 'ready for connections'; - return new Promise( resolve => { - const interval = setInterval( () => { - console.log( 'Waiting for mysql...' ); - exec( `docker logs ${ containerName }`, ( error, stdout, stderr ) => { - if ( error ) { - console.error( 'Error exporting database!' ); + return new Promise((resolve) => { + const interval = setInterval(() => { + console.log('Waiting for mysql...'); + exec(`docker logs ${containerName}`, (error, stdout, stderr) => { + if (error) { + console.error('Error exporting database!'); process.exit(); } - if ( stderr.indexOf( readyMatch ) !== -1 ) { - clearInterval( interval ); + if (stderr.indexOf(readyMatch) !== -1) { + clearInterval(interval); resolve(); } - } ); - }, 1000 ); - } ); + }); + }, 1000); + }); } -async function exportOldDatabase( oldEnv, exportDir ) { - const dataDir = path.join( oldEnv, 'data', 'db' ); - const parts = path.parse( oldEnv ); - const base = `mysql-${ parts.name }`; +async function exportOldDatabase(oldEnv, exportDir) { + const dataDir = path.join(oldEnv, 'data', 'db'); + const parts = path.parse(oldEnv); + const base = `mysql-${parts.name}`; // Just in case this failed and are retrying try { - execSync( `docker stop ${ base }`, { stdio: 'ignore' } ); - execSync( `docker rm ${ base }`, { stdio: 'ignore' } ); - } catch { - } + execSync(`docker stop ${base}`, { stdio: 'ignore' }); + execSync(`docker rm ${base}`, { stdio: 'ignore' }); + } catch {} try { - execSync( `docker run -d --rm --name ${ base } -v ${ dataDir }:/var/lib/mysql -v ${ exportDir }:/tmp/export mysql:5`, { stdio: 'inherit' } ); - await waitForDB( base ); - console.log( 'Exporting old database' ); - execSync( `docker exec ${ base } sh -c "/usr/bin/mysqldump -u root -ppassword wordpress > /tmp/export/database.sql"`, { stdio: 'inherit' } ); - execSync( `docker stop ${ base }` ); - } catch { - } + execSync( + `docker run -d --rm --name ${base} -v ${dataDir}:/var/lib/mysql -v ${exportDir}:/tmp/export mysql:5`, + { stdio: 'inherit' }, + ); + await waitForDB(base); + console.log('Exporting old database'); + execSync( + `docker exec ${base} sh -c "/usr/bin/mysqldump -u root -ppassword wordpress > /tmp/export/database.sql"`, + { stdio: 'inherit' }, + ); + execSync(`docker stop ${base}`); + } catch {} } -async function importNewDatabase( env, spinner ) { - await start( env, spinner ); - const envPath = await envUtils.getPathOrError( env ); +async function importNewDatabase(env, spinner) { + await start(env, spinner); + const envPath = await envUtils.getPathOrError(env); try { - console.log( 'Importing DB to new Environment' ); - execSync( 'docker-compose exec --user www-data phpfpm wp db import /var/www/html/import/database.sql', { stdio: 'inherit', cwd: envPath } ); - } catch { - } + console.log('Importing DB to new Environment'); + execSync( + 'docker-compose exec --user www-data phpfpm wp db import /var/www/html/import/database.sql', + { stdio: 'inherit', cwd: envPath }, + ); + } catch {} } -async function copySiteFiles( oldEnv, newEnv ) { - const envPath = await envUtils.getPathOrError( newEnv ); - const wpContent = path.join( envPath, 'wordpress', 'wp-content' ); - const oldWpContent = path.join( oldEnv, 'wordpress', 'wp-content' ); +async function copySiteFiles(oldEnv, newEnv) { + const envPath = await envUtils.getPathOrError(newEnv); + const wpContent = path.join(envPath, 'wordpress', 'wp-content'); + const oldWpContent = path.join(oldEnv, 'wordpress', 'wp-content'); // Clear out all the current environment content // Only doing wp-content for now, since otherwise we would need to keep wp-config.php... But what about customizations? - console.log( `Removing current files from ${ wpContent }` ); - await fs.emptyDir( wpContent ); + console.log(`Removing current files from ${wpContent}`); + await fs.emptyDir(wpContent); - console.log( 'Copying files from the old wp-content folder' ); - await fs.copy( oldWpContent, wpContent ); + console.log('Copying files from the old wp-content folder'); + await fs.copy(oldWpContent, wpContent); } -exports.handler = makeCommand( async function ( { old, env, verbose } ) { +exports.handler = makeCommand(async function ({ old, env, verbose }) { const spinner = !verbose ? makeSpinner() : undefined; - const oldEnv = path.resolve( old ); + const oldEnv = path.resolve(old); // So that we don't prompt at every step... let envName = env; - if ( !envName ) { + if (!envName) { envName = await envUtils.promptEnv(); } // Validate old environment - if ( ! await fs.pathExists( path.join( oldEnv, 'docker-compose.yml' ) ) ) { - throw new Error( 'Could not find a docker-compose.yml file in the path specified for the old environment!' ); + if (!(await fs.pathExists(path.join(oldEnv, 'docker-compose.yml')))) { + throw new Error( + 'Could not find a docker-compose.yml file in the path specified for the old environment!', + ); } - if ( ! await fs.pathExists( path.join( oldEnv, 'data', 'db' ) ) ) { - throw new Error( 'Could not find MySQL data in the path specified for the old environment!' ); + if (!(await fs.pathExists(path.join(oldEnv, 'data', 'db')))) { + throw new Error('Could not find MySQL data in the path specified for the old environment!'); } // Stopping old environment - if ( spinner ) { - spinner.start( 'Ensuring old environment is stopped...' ); + if (spinner) { + spinner.start('Ensuring old environment is stopped...'); } else { - console.log( 'Ensuring old environment is not running...' ); + console.log('Ensuring old environment is not running...'); } - await compose.down( { + await compose.down({ cwd: oldEnv, log: !spinner, - } ); + }); - if ( spinner ) { - spinner.succeed( 'Old environment is stopped...' ); + if (spinner) { + spinner.succeed('Old environment is stopped...'); } // So that the DB is already in the folder mounted to docker - const envPath = await envUtils.getPathOrError( envName ); - const exportDir = path.join( envPath, 'wordpress', 'import' ); - await fs.ensureDir( exportDir ); - - await exportOldDatabase( oldEnv, exportDir ); - await importNewDatabase( envName, spinner ); - await copySiteFiles( oldEnv, envName ); - - console.log( `${ chalk.bold.green( 'Success!' ) } Your environment has been imported!` ); - console.log( ' - wp-config.php has not been changed. Any custom configuration needs to be manually copied' ); - console.log( ' - If you need to run a search/replace, run `wpdocker wp search-replace `' ); -} ); + const envPath = await envUtils.getPathOrError(envName); + const exportDir = path.join(envPath, 'wordpress', 'import'); + await fs.ensureDir(exportDir); + + await exportOldDatabase(oldEnv, exportDir); + await importNewDatabase(envName, spinner); + await copySiteFiles(oldEnv, envName); + + console.log(`${chalk.bold.green('Success!')} Your environment has been imported!`); + console.log( + ' - wp-config.php has not been changed. Any custom configuration needs to be manually copied', + ); + console.log( + ' - If you need to run a search/replace, run `wpdocker wp search-replace `', + ); +}); diff --git a/src/commands/postinstall.js b/src/commands/postinstall.js index 62a0c35c..ea119ad4 100644 --- a/src/commands/postinstall.js +++ b/src/commands/postinstall.js @@ -1,48 +1,50 @@ -const { join } = require( 'path' ); +const { join } = require('path'); -const fsExtra = require( 'fs-extra' ); +const fsExtra = require('fs-extra'); -const { installCA } = require( '../certificates' ); -const { getGlobalDirectory } = require( '../configure' ); -const { writeYaml, readYaml } = require( '../utils/yaml' ); -const makeCommand = require( '../utils/make-command' ); +const { installCA } = require('../certificates'); +const { getGlobalDirectory } = require('../configure'); +const { writeYaml, readYaml } = require('../utils/yaml'); +const makeCommand = require('../utils/make-command'); exports.command = 'postinstall'; exports.desc = false; // makes this command hidden -exports.handler = makeCommand( async function() { - const isLocal = await fsExtra.pathExists( join( __dirname, '../../package-lock.json' ) ); - if ( isLocal ) { +exports.handler = makeCommand(async function () { + const isLocal = await fsExtra.pathExists(join(__dirname, '../../package-lock.json')); + if (isLocal) { return; } // install CA in the trust store - installCA( true ); + installCA(true); // add ssl-certs directory to the global docker-compose.yml const globalDir = getGlobalDirectory(); - const globalDockerCompose = join( globalDir, 'docker-compose.yml' ); - const globalDockerComposeExists = await fsExtra.pathExists( globalDockerCompose ); - if ( globalDockerComposeExists ) { + const globalDockerCompose = join(globalDir, 'docker-compose.yml'); + const globalDockerComposeExists = await fsExtra.pathExists(globalDockerCompose); + if (globalDockerComposeExists) { let changed = false; - const yaml = readYaml( globalDockerCompose ); + const yaml = readYaml(globalDockerCompose); - if ( yaml && yaml.services && yaml.services.gateway ) { - if ( ! yaml.services.gateway.volumes || ! Array.isArray( yaml.services.gateway.volumes ) ) { + if (yaml && yaml.services && yaml.services.gateway) { + if (!yaml.services.gateway.volumes || !Array.isArray(yaml.services.gateway.volumes)) { yaml.services.gateway.volumes = []; } - const exists = yaml.services.gateway.volumes.some( ( volume ) => volume.split( ':' )[0] === './ssl-certs' ); - if ( ! exists ) { - yaml.services.gateway.volumes.push( './ssl-certs:/etc/nginx/certs:ro' ); + const exists = yaml.services.gateway.volumes.some( + (volume) => volume.split(':')[0] === './ssl-certs', + ); + if (!exists) { + yaml.services.gateway.volumes.push('./ssl-certs:/etc/nginx/certs:ro'); changed = true; } } - if ( changed ) { - await writeYaml( globalDockerCompose, yaml ).catch( ( err ) => { - console.error( err ); - } ); + if (changed) { + await writeYaml(globalDockerCompose, yaml).catch((err) => { + console.error(err); + }); } } -} ); +}); diff --git a/src/commands/restart.js b/src/commands/restart.js index b7af1249..0f5f336c 100644 --- a/src/commands/restart.js +++ b/src/commands/restart.js @@ -1,26 +1,26 @@ -const makeCommand = require( '../utils/make-command' ); -const makeSpinner = require( '../utils/make-spinner' ); -const { resolveEnvironment } = require( '../env-utils' ); -const { restart, restartAll } = require( '../environment' ); +const makeCommand = require('../utils/make-command'); +const makeSpinner = require('../utils/make-spinner'); +const { resolveEnvironment } = require('../env-utils'); +const { restart, restartAll } = require('../environment'); exports.command = 'restart []'; exports.desc = 'Restarts a specific docker environment.'; -exports.builder = function( yargs ) { - yargs.positional( 'env', { +exports.builder = function (yargs) { + yargs.positional('env', { type: 'string', describe: 'Optional. Environment name.', - } ); + }); }; -exports.handler = makeCommand( async ( { verbose, env } ) => { - const spinner = ! verbose ? makeSpinner() : undefined; +exports.handler = makeCommand(async ({ verbose, env }) => { + const spinner = !verbose ? makeSpinner() : undefined; const all = env === 'all'; - if ( all ) { - await restartAll( spinner ); + if (all) { + await restartAll(spinner); } else { - const envName = await resolveEnvironment( env || '' ); - await restart( envName, spinner ); + const envName = await resolveEnvironment(env || ''); + await restart(envName, spinner); } -} ); +}); diff --git a/src/commands/shell.js b/src/commands/shell.js index a24e57a6..a421b1bb 100644 --- a/src/commands/shell.js +++ b/src/commands/shell.js @@ -1,55 +1,57 @@ -const { execSync } = require( 'child_process' ); +const { execSync } = require('child_process'); -const envUtils = require( '../env-utils' ); -const gateway = require( '../gateway' ); -const environment = require( '../environment' ); -const makeCommand = require( '../utils/make-command' ); -const makeSpinner = require( '../utils/make-spinner' ); -const compose = require( '../utils/docker-compose' ); +const envUtils = require('../env-utils'); +const gateway = require('../gateway'); +const environment = require('../environment'); +const makeCommand = require('../utils/make-command'); +const makeSpinner = require('../utils/make-spinner'); +const compose = require('../utils/docker-compose'); exports.command = 'shell [] []'; -exports.desc = 'Opens a shell for a specified container in your current environment (Defaults to the phpfpm container).'; +exports.desc = + 'Opens a shell for a specified container in your current environment (Defaults to the phpfpm container).'; -exports.builder = function( yargs ) { - yargs.positional( 'container', { +exports.builder = function (yargs) { + yargs.positional('container', { describe: 'Container name', type: 'string', default: 'phpfpm', - } ); + }); - yargs.positional( 'cmd', { + yargs.positional('cmd', { describe: 'Command to run', type: 'string', default: 'bash', - } ); + }); }; -exports.handler = makeCommand( async ( { container, cmd, env, verbose } ) => { +exports.handler = makeCommand(async ({ container, cmd, env, verbose }) => { let envSlug = env; - if ( ! envSlug ) { + if (!envSlug) { envSlug = await envUtils.parseOrPromptEnv(); } - if ( envSlug === false ) { - throw new Error( 'Error: Unable to determine which environment to open a shell for. Please run this command from within your environment.' ); + if (envSlug === false) { + throw new Error( + 'Error: Unable to determine which environment to open a shell for. Please run this command from within your environment.', + ); } - const envPath = await envUtils.envPath( envSlug ); - const spinner = ! verbose ? makeSpinner() : undefined; + const envPath = await envUtils.envPath(envSlug); + const spinner = !verbose ? makeSpinner() : undefined; // Check if the container is running, otherwise, start up the stacks - const isRunning = await compose.isRunning( envPath ); - if ( ! isRunning ) { - spinner && spinner.info( 'Environment is not running, starting it...' ); - await gateway.startGlobal( spinner ); - await environment.start( envSlug, spinner ); + const isRunning = await compose.isRunning(envPath); + if (!isRunning) { + spinner && spinner.info('Environment is not running, starting it...'); + await gateway.startGlobal(spinner); + await environment.start(envSlug, spinner); } try { - execSync( `docker-compose exec ${ container } ${ cmd }`, { + execSync(`docker-compose exec ${container} ${cmd}`, { stdio: 'inherit', cwd: envPath, - } ); - } catch { - } -} ); + }); + } catch {} +}); diff --git a/src/commands/start.js b/src/commands/start.js index a34249df..495c9e3d 100644 --- a/src/commands/start.js +++ b/src/commands/start.js @@ -1,63 +1,63 @@ -const { EOL } = require( 'os' ); +const { EOL } = require('os'); -const makeCommand = require( '../utils/make-command' ); -const makeSpinner = require( '../utils/make-spinner' ); -const makeBoxen = require( '../utils/make-boxen' ); -const { replaceLinks } = require( '../utils/make-link' ); +const makeCommand = require('../utils/make-command'); +const makeSpinner = require('../utils/make-spinner'); +const makeBoxen = require('../utils/make-boxen'); +const { replaceLinks } = require('../utils/make-link'); -const envUtils = require( '../env-utils' ); -const { startAll, start } = require( '../environment' ); +const envUtils = require('../env-utils'); +const { startAll, start } = require('../environment'); exports.command = 'start [] [--pull]'; exports.desc = 'Starts a specific docker environment.'; -exports.builder = function( yargs ) { - yargs.positional( 'env', { +exports.builder = function (yargs) { + yargs.positional('env', { type: 'string', describe: 'Optional. Environment name.', - } ); + }); - yargs.option( 'pull', { + yargs.option('pull', { description: 'Pull images when environment starts', default: false, type: 'boolean', - } ); + }); }; -exports.handler = makeCommand( async ( { verbose, pull, env } ) => { - const spinner = ! verbose ? makeSpinner() : undefined; +exports.handler = makeCommand(async ({ verbose, pull, env }) => { + const spinner = !verbose ? makeSpinner() : undefined; const all = env === 'all'; - if ( all ) { - await startAll( spinner, pull ); + if (all) { + await startAll(spinner, pull); } else { - const envName = await envUtils.resolveEnvironment( env || '' ); - await start( envName, spinner, pull ); + const envName = await envUtils.resolveEnvironment(env || ''); + await start(envName, spinner, pull); - const envPath = await envUtils.getPathOrError( envName, spinner ); - const envHosts = await envUtils.getEnvHosts( envPath ); - const certs = await envUtils.getEnvConfig( envPath, 'certs' ); - const phpVersion = await envUtils.getEnvPhpVersion( envPath ); - await envUtils.checkForEOLPHP( phpVersion ); + const envPath = await envUtils.getPathOrError(envName, spinner); + const envHosts = await envUtils.getEnvHosts(envPath); + const certs = await envUtils.getEnvConfig(envPath, 'certs'); + const phpVersion = await envUtils.getEnvPhpVersion(envPath); + await envUtils.checkForEOLPHP(phpVersion); - if ( Array.isArray( envHosts ) && envHosts.length > 0 ) { + if (Array.isArray(envHosts) && envHosts.length > 0) { let info = ''; const links = {}; const http = certs ? 'https' : 'http'; - envHosts.forEach( ( host ) => { - const home = `${ http }://${ host }/`; - const admin = `${ http }://${ host }/wp-admin/`; + envHosts.forEach((host) => { + const home = `${http}://${host}/`; + const admin = `${http}://${host}/wp-admin/`; - links[ home ] = home; - links[ admin ] = admin; + links[home] = home; + links[admin] = admin; - info += `Homepage: ${ home }${ EOL }`; - info += `WP admin: ${ admin }${ EOL }`; + info += `Homepage: ${home}${EOL}`; + info += `WP admin: ${admin}${EOL}`; info += EOL; - } ); + }); - console.log( replaceLinks( makeBoxen()( info ), links ) ); + console.log(replaceLinks(makeBoxen()(info), links)); } } -} ); +}); diff --git a/src/commands/stop.js b/src/commands/stop.js index 395c465a..c7f6ee9c 100644 --- a/src/commands/stop.js +++ b/src/commands/stop.js @@ -1,26 +1,26 @@ -const makeCommand = require( '../utils/make-command' ); -const makeSpinner = require( '../utils/make-spinner' ); -const { resolveEnvironment } = require( '../env-utils' ); -const { stop, stopAll } = require( '../environment' ); +const makeCommand = require('../utils/make-command'); +const makeSpinner = require('../utils/make-spinner'); +const { resolveEnvironment } = require('../env-utils'); +const { stop, stopAll } = require('../environment'); exports.command = 'stop []'; exports.desc = 'Stops a specific docker environment.'; -exports.builder = function( yargs ) { - yargs.positional( 'env', { +exports.builder = function (yargs) { + yargs.positional('env', { type: 'string', describe: 'Optional. Environment name.', - } ); + }); }; -exports.handler = makeCommand( async ( { verbose, env } ) => { - const spinner = ! verbose ? makeSpinner() : undefined; +exports.handler = makeCommand(async ({ verbose, env }) => { + const spinner = !verbose ? makeSpinner() : undefined; const all = env === 'all'; - if ( all ) { - await stopAll( spinner ); + if (all) { + await stopAll(spinner); } else { - const envName = await resolveEnvironment( env || '' ); - await stop( envName, spinner ); + const envName = await resolveEnvironment(env || ''); + await stop(envName, spinner); } -} ); +}); diff --git a/src/commands/upgrade.js b/src/commands/upgrade.js index 32002d6b..e2423224 100644 --- a/src/commands/upgrade.js +++ b/src/commands/upgrade.js @@ -1,53 +1,53 @@ -const path = require( 'path' ); -const os = require( 'os' ); -const inquirer = require( 'inquirer' ); +const path = require('path'); +const os = require('os'); +const inquirer = require('inquirer'); -const fsExtra = require( 'fs-extra' ); -const chalk = require( 'chalk' ); +const fsExtra = require('fs-extra'); +const chalk = require('chalk'); -const makeCommand = require( '../utils/make-command' ); -const makeSpinner = require( '../utils/make-spinner' ); -const makeDocker = require( '../utils/make-docker' ); -const { readYaml, writeYaml } = require( '../utils/yaml' ); -const envUtils = require( '../env-utils' ); -const { images } = require( '../docker-images' ); -const { stop, start } = require( '../environment' ); +const makeCommand = require('../utils/make-command'); +const makeSpinner = require('../utils/make-spinner'); +const makeDocker = require('../utils/make-docker'); +const { readYaml, writeYaml } = require('../utils/yaml'); +const envUtils = require('../env-utils'); +const { images } = require('../docker-images'); +const { stop, start } = require('../environment'); exports.command = 'upgrade []'; exports.desc = 'Upgrades environment to the latest version.'; -exports.builder = function( yargs ) { - yargs.positional( 'env', { +exports.builder = function (yargs) { + yargs.positional('env', { type: 'string', describe: 'Optional. Environment name.', - } ); + }); }; -exports.handler = makeCommand( { checkDocker: false }, async ( { verbose, env } ) => { - const spinner = ! verbose ? makeSpinner() : undefined; - const envName = await envUtils.resolveEnvironment( env || '' ); - const envPath = await envUtils.getPathOrError( envName, spinner ); - const envSlug = envUtils.envSlug( envName ); +exports.handler = makeCommand({ checkDocker: false }, async ({ verbose, env }) => { + const spinner = !verbose ? makeSpinner() : undefined; + const envName = await envUtils.resolveEnvironment(env || ''); + const envPath = await envUtils.getPathOrError(envName, spinner); + const envSlug = envUtils.envSlug(envName); - await stop( envSlug, spinner ); + await stop(envSlug, spinner); // Create a backup of the old yaml. - const dockerCompose = path.join( envPath, 'docker-compose.yml' ); - const yaml = readYaml( dockerCompose ); + const dockerCompose = path.join(envPath, 'docker-compose.yml'); + const yaml = readYaml(dockerCompose); try { - const dockerComposeBak = `${ dockerCompose }.bak`; - await writeYaml( dockerComposeBak, yaml ); - if ( spinner ) { - spinner.info( `Backup is created: ${ chalk.cyan( dockerComposeBak ) }` ); + const dockerComposeBak = `${dockerCompose}.bak`; + await writeYaml(dockerComposeBak, yaml); + if (spinner) { + spinner.info(`Backup is created: ${chalk.cyan(dockerComposeBak)}`); } else { - console.log( `Created backup of previous configuration ${ envSlug }` ); + console.log(`Created backup of previous configuration ${envSlug}`); } - } catch ( err ) { - if ( spinner ) { + } catch (err) { + if (spinner) { throw err; } else { - console.log( err ); + console.log(err); } } @@ -55,39 +55,44 @@ exports.handler = makeCommand( { checkDocker: false }, async ( { verbose, env } yaml.version = '2.2'; // Upgrade image. - const phpVersion = yaml.services.phpfpm.image.split( ':' ).pop(); - if ( phpVersion ) { - if ( phpVersion === '5.5' ) { - if ( spinner ) { - spinner.warn( 'Support for PHP v5.5 was removed in the latest version of WP Docker.' ); - throw new Error( 'This environment cannot be upgraded. No changes were made.' ); + const phpVersion = yaml.services.phpfpm.image.split(':').pop(); + if (phpVersion) { + if (phpVersion === '5.5') { + if (spinner) { + spinner.warn( + 'Support for PHP v5.5 was removed in the latest version of WP Docker.', + ); + throw new Error('This environment cannot be upgraded. No changes were made.'); } else { - console.warn( 'Support for PHP v5.5 was removed in the latest version of WP Docker.' ); - console.error( 'This environment cannot be upgraded. No changes were made.' ); - process.exit( 1 ); + console.warn( + 'Support for PHP v5.5 was removed in the latest version of WP Docker.', + ); + console.error('This environment cannot be upgraded. No changes were made.'); + process.exit(1); } } } - const phpImage = images[`php${ phpVersion }`]; - if ( phpImage ) { + const phpImage = images[`php${phpVersion}`]; + if (phpImage) { yaml.services.phpfpm.image = phpImage; } - if ( yaml.services.elasticsearch !== undefined ) { - const elasticVersion = yaml.services.elasticsearch.image.split( ':' ).pop(); - if ( elasticVersion === '7.9.3' ) { - spinner.succeed( 'elasticsearch image is on the lastest supported verison.' ); + if (yaml.services.elasticsearch !== undefined) { + const elasticVersion = yaml.services.elasticsearch.image.split(':').pop(); + if (elasticVersion === '7.9.3') { + spinner.succeed('elasticsearch image is on the lastest supported verison.'); } else { - const { upgradeElastic } = await inquirer.prompt( { + const { upgradeElastic } = await inquirer.prompt({ name: 'upgradeElastic', type: 'confirm', - message: 'Do you want to upgrade elasticsearch image? This will delete all data on that volume. You will need to reindex.', - default: false - } ); - if ( upgradeElastic === true ) { + message: + 'Do you want to upgrade elasticsearch image? This will delete all data on that volume. You will need to reindex.', + default: false, + }); + if (upgradeElastic === true) { const docker = makeDocker(); - const volume = await docker.getVolume( `${ envSlug }_elasticsearchData` ); + const volume = await docker.getVolume(`${envSlug}_elasticsearchData`); await volume.remove(); const elasticImage = images['elasticsearch']; yaml.services.elasticsearch.image = elasticImage; @@ -96,56 +101,68 @@ exports.handler = makeCommand( { checkDocker: false }, async ( { verbose, env } } // Update defined services to have all cached volumes - [ 'nginx', 'phpfpm', 'elasticsearch' ].forEach( ( service ) => { - if ( yaml.services[ service ] && Array.isArray( yaml.services[ service ].volumes ) ) { - yaml.services[ service ].volumes.forEach( ( volume, index ) => { - const parts = volume.split( ':' ); - if ( parts.length === 2 ) { - parts.push( 'cached' ); - yaml.services[ service ].volumes[ index ] = parts.join( ':' ); + ['nginx', 'phpfpm', 'elasticsearch'].forEach((service) => { + if (yaml.services[service] && Array.isArray(yaml.services[service].volumes)) { + yaml.services[service].volumes.forEach((volume, index) => { + const parts = volume.split(':'); + if (parts.length === 2) { + parts.push('cached'); + yaml.services[service].volumes[index] = parts.join(':'); } - } ); + }); } - } ); - const phpVersionNumber = phpVersion.split( '-' ).shift(); + }); + const phpVersionNumber = phpVersion.split('-').shift(); // Upgrade volume mounts. const deprecatedVolumes = [ './config/php-fpm/php.ini:/usr/local/etc/php/php.ini:cached', './config/php-fpm/docker-php-ext-xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini:cached', - `./config/php-fpm/docker-php-ext-xdebug.ini:/etc/php.d/${ phpVersionNumber }/fpm/docker-php-ext-xdebug.ini:cached`, + `./config/php-fpm/docker-php-ext-xdebug.ini:/etc/php.d/${phpVersionNumber}/fpm/docker-php-ext-xdebug.ini:cached`, '~/.ssh:/root/.ssh:cached', '~/.ssh:/home/www-data/.ssh:cached', - `~/.ssh:/home/${ process.env.USER }/.ssh:cached` // For Linux compatibility + `~/.ssh:/home/${process.env.USER}/.ssh:cached`, // For Linux compatibility ]; - const volumes = [ ...yaml.services.phpfpm.volumes ]; - yaml.services.phpfpm.volumes = volumes.reduce( ( acc, curr ) => { - if ( deprecatedVolumes.includes( curr ) ) { + const volumes = [...yaml.services.phpfpm.volumes]; + yaml.services.phpfpm.volumes = volumes.reduce((acc, curr) => { + if (deprecatedVolumes.includes(curr)) { // Replace xdebug config volume to be mounted to the new location. - if ( curr === './config/php-fpm/docker-php-ext-xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini:cached' ) { - acc.push( './config/php-fpm/docker-php-ext-xdebug.ini:/etc/php.d/docker-php-ext-xdebug.ini:cached' ); + if ( + curr === + './config/php-fpm/docker-php-ext-xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini:cached' + ) { + acc.push( + './config/php-fpm/docker-php-ext-xdebug.ini:/etc/php.d/docker-php-ext-xdebug.ini:cached', + ); } - if ( curr == `./config/php-fpm/docker-php-ext-xdebug.ini:/etc/php.d/${ phpVersionNumber }/fpm/docker-php-ext-xdebug.ini:cached` ) { - acc.push( `./config/php-fpm/docker-php-ext-xdebug.ini:/etc/php/${ phpVersionNumber }/fpm/conf.d/docker-php-ext-xdebug.ini:cached` ); + if ( + curr == + `./config/php-fpm/docker-php-ext-xdebug.ini:/etc/php.d/${phpVersionNumber}/fpm/docker-php-ext-xdebug.ini:cached` + ) { + acc.push( + `./config/php-fpm/docker-php-ext-xdebug.ini:/etc/php/${phpVersionNumber}/fpm/conf.d/docker-php-ext-xdebug.ini:cached`, + ); } } else { - acc.push( curr ); + acc.push(curr); } return acc; - }, [] ); + }, []); // Update environmental variables. yaml.services.phpfpm.environment = yaml.services.phpfpm.environment || {}; - yaml.services.phpfpm.environment.ENABLE_XDEBUG = yaml.services.phpfpm.environment.ENABLE_XDEBUG || 'true'; - yaml.services.phpfpm.environment.PHP_IDE_CONFIG = yaml.services.phpfpm.environment.PHP_IDE_CONFIG || `serverName=${ envSlug }`; + yaml.services.phpfpm.environment.ENABLE_XDEBUG = + yaml.services.phpfpm.environment.ENABLE_XDEBUG || 'true'; + yaml.services.phpfpm.environment.PHP_IDE_CONFIG = + yaml.services.phpfpm.environment.PHP_IDE_CONFIG || `serverName=${envSlug}`; // Add appropriate capabilities to php container - if ( ! Array.isArray( yaml.services.phpfpm.cap_add ) ) { + if (!Array.isArray(yaml.services.phpfpm.cap_add)) { yaml.services.phpfpm.cap_add = []; } - if ( ! yaml.services.phpfpm.cap_add.includes( 'SYS_PTRACE' ) ) { - yaml.services.phpfpm.cap_add.push( 'SYS_PTRACE' ); + if (!yaml.services.phpfpm.cap_add.includes('SYS_PTRACE')) { + yaml.services.phpfpm.cap_add.push('SYS_PTRACE'); } // Unlike Mac and Windows, Docker is a first class citizen on Linux @@ -153,49 +170,56 @@ exports.handler = makeCommand( { checkDocker: false }, async ( { verbose, env } // file system. Because of this the phpfpm container will be running as the // wrong user. Here we setup the docker-compose.yml file to rebuild the // phpfpm container so that it runs as the user who created the project. - if ( os.platform() == 'linux' ) { - if ( phpVersion && phpImage ) { - yaml.services.phpfpm.image = `wp-php-fpm-dev-${ phpVersion }-${ process.env.USER }`; + if (os.platform() == 'linux') { + if (phpVersion && phpImage) { + yaml.services.phpfpm.image = `wp-php-fpm-dev-${phpVersion}-${process.env.USER}`; yaml.services.phpfpm.build = { dockerfile: 'php-fpm', context: '.containers', args: { - PHP_IMAGE: images[`php${ phpVersion }`], + PHP_IMAGE: images[`php${phpVersion}`], CALLING_USER: process.env.USER, CALLING_UID: process.getuid(), }, }; } - yaml.services.phpfpm.volumes.push( `~/.ssh:/home/${ process.env.USER }/.ssh:cached` ); + yaml.services.phpfpm.volumes.push(`~/.ssh:/home/${process.env.USER}/.ssh:cached`); } else { // the official containers for this project will have a www-data user. - yaml.services.phpfpm.volumes.push( '~/.ssh:/home/www-data/.ssh:cached' ); + yaml.services.phpfpm.volumes.push('~/.ssh:/home/www-data/.ssh:cached'); } // Remove legacy memcacheAdmin image delete yaml.services.memcacheadmin; - if ( yaml.services.nginx && Array.isArray( yaml.services.nginx['depends_on'] ) ) { - yaml.services.nginx['depends_on'] = yaml.services.nginx.depends_on.filter( ( service ) => service !== 'memcacheadmin' ); + if (yaml.services.nginx && Array.isArray(yaml.services.nginx['depends_on'])) { + yaml.services.nginx['depends_on'] = yaml.services.nginx.depends_on.filter( + (service) => service !== 'memcacheadmin', + ); } - if ( ! spinner ) { - console.log( 'Copying required files...' ); + if (!spinner) { + console.log('Copying required files...'); } - await fsExtra.ensureDir( path.join( envPath, '.containers' ) ); - await fsExtra.copy( path.join( envUtils.srcPath, 'containers' ), path.join( envPath, '.containers' ) ); + await fsExtra.ensureDir(path.join(envPath, '.containers')); + await fsExtra.copy( + path.join(envUtils.srcPath, 'containers'), + path.join(envPath, '.containers'), + ); try { - await writeYaml( dockerCompose, yaml ); - if ( spinner ) { - spinner.succeed( `${ chalk.cyan( envSlug ) } is upgraded to the latest version of WP Docker` ); + await writeYaml(dockerCompose, yaml); + if (spinner) { + spinner.succeed( + `${chalk.cyan(envSlug)} is upgraded to the latest version of WP Docker`, + ); } else { - console.log( `${ envSlug } is upgraded to the latest version of WP Docker...` ); + console.log(`${envSlug} is upgraded to the latest version of WP Docker...`); } - } catch ( err ) { - console.error( err ); + } catch (err) { + console.error(err); } - await start( envSlug, spinner ); -} ); + await start(envSlug, spinner); +}); diff --git a/src/commands/wp.js b/src/commands/wp.js index 8a7ba94b..63f796a5 100644 --- a/src/commands/wp.js +++ b/src/commands/wp.js @@ -1,55 +1,57 @@ -const { execSync } = require( 'child_process' ); +const { execSync } = require('child_process'); -const shellEscape = require( 'shell-escape' ); +const shellEscape = require('shell-escape'); -const envUtils = require( '../env-utils' ); -const gateway = require( '../gateway' ); -const environment = require( '../environment' ); -const makeCommand = require( '../utils/make-command' ); -const makeSpinner = require( '../utils/make-spinner' ); -const compose = require( '../utils/docker-compose' ); +const envUtils = require('../env-utils'); +const gateway = require('../gateway'); +const environment = require('../environment'); +const makeCommand = require('../utils/make-command'); +const makeSpinner = require('../utils/make-spinner'); +const compose = require('../utils/docker-compose'); exports.command = 'wp '; exports.desc = 'Runs a wp-cli command in your environment.'; -exports.builder = function( yargs ) { - yargs.positional( 'cmd', { +exports.builder = function (yargs) { + yargs.positional('cmd', { describe: 'Command to run', type: 'string', - } ); + }); }; -exports.handler = makeCommand( async ( { verbose, env } ) => { +exports.handler = makeCommand(async ({ verbose, env }) => { let envSlug = env; - if ( ! envSlug ) { + if (!envSlug) { envSlug = await envUtils.parseOrPromptEnv(); } - if ( envSlug === false ) { - throw new Error( 'Error: Unable to determine which environment to use WP CLI with. Please run this command from within your environment\'s directory.' ); + if (envSlug === false) { + throw new Error( + "Error: Unable to determine which environment to use WP CLI with. Please run this command from within your environment's directory.", + ); } - const envPath = await envUtils.envPath( envSlug ); - const spinner = ! verbose ? makeSpinner() : undefined; + const envPath = await envUtils.envPath(envSlug); + const spinner = !verbose ? makeSpinner() : undefined; // Check if the container is running, otherwise, start up the stacks - const isRunning = await compose.isRunning( envPath ); - if ( ! isRunning ) { - spinner && spinner.info( 'Environment is not running, starting it...' ); - await gateway.startGlobal( spinner ); - await environment.start( envSlug, spinner ); + const isRunning = await compose.isRunning(envPath); + if (!isRunning) { + spinner && spinner.info('Environment is not running, starting it...'); + await gateway.startGlobal(spinner); + await environment.start(envSlug, spinner); } // Compose wp-cli command to run let wpCommand = false; const command = []; - for ( let i = 0; i < process.argv.length; i++ ) { - if ( process.argv[i].toLowerCase() === 'wp' ) { + for (let i = 0; i < process.argv.length; i++) { + if (process.argv[i].toLowerCase() === 'wp') { wpCommand = true; } - if ( wpCommand ) { - command.push( process.argv[i] ); + if (wpCommand) { + command.push(process.argv[i]); } } @@ -58,11 +60,11 @@ exports.handler = makeCommand( async ( { verbose, env } ) => { const ttyFlag = process.stdin.isTTY ? '' : ' -T'; // Run the command - execSync( `docker-compose exec${ ttyFlag } phpfpm ${ shellEscape( command ) }`, { + execSync(`docker-compose exec${ttyFlag} phpfpm ${shellEscape(command)}`, { stdio: 'inherit', - cwd: envPath - } ); + cwd: envPath, + }); } catch { // do nothing } -} ); +}); diff --git a/src/commands/wpsnapshots.js b/src/commands/wpsnapshots.js index 2fda62eb..3ad311c8 100644 --- a/src/commands/wpsnapshots.js +++ b/src/commands/wpsnapshots.js @@ -1,59 +1,61 @@ -const { execSync } = require( 'child_process' ); +const { execSync } = require('child_process'); -const makeCommand = require( '../utils/make-command' ); -const makeSpinner = require( '../utils/make-spinner' ); -const shellEscape = require( 'shell-escape' ); -const envUtils = require( '../env-utils' ); -const gateway = require( '../gateway' ); -const environment = require( '../environment' ); -const compose = require( '../utils/docker-compose' ); +const makeCommand = require('../utils/make-command'); +const makeSpinner = require('../utils/make-spinner'); +const shellEscape = require('shell-escape'); +const envUtils = require('../env-utils'); +const gateway = require('../gateway'); +const environment = require('../environment'); +const compose = require('../utils/docker-compose'); exports.command = 'wpsnapshots '; -exports.aliases = [ 'snapshots' ]; +exports.aliases = ['snapshots']; exports.desc = 'Runs a wp snapshots command.'; -exports.builder = function( yargs ) { - yargs.positional( 'cmd', { +exports.builder = function (yargs) { + yargs.positional('cmd', { describe: 'Command to run', type: 'string', - } ); + }); }; -exports.handler = makeCommand( async function( { env, verbose } ) { +exports.handler = makeCommand(async function ({ env, verbose }) { let envSlug = env; - if ( ! envSlug ) { + if (!envSlug) { envSlug = await envUtils.parseOrPromptEnv(); } - if ( envSlug === false ) { - throw new Error( 'Error: Unable to determine which environment to use snapshots with. Please run this command from within your environment\'s directory.' ); + if (envSlug === false) { + throw new Error( + "Error: Unable to determine which environment to use snapshots with. Please run this command from within your environment's directory.", + ); } - const envPath = await envUtils.envPath( envSlug ); - const spinner = ! verbose ? makeSpinner() : undefined; + const envPath = await envUtils.envPath(envSlug); + const spinner = !verbose ? makeSpinner() : undefined; // Check if the container is running, otherwise, start up the stacks - const isRunning = await compose.isRunning( envPath ); - if ( ! isRunning ) { - spinner && spinner.info( 'Environment is not running, starting it...' ); - await gateway.startGlobal( spinner ); - await environment.start( envSlug, spinner ); + const isRunning = await compose.isRunning(envPath); + if (!isRunning) { + spinner && spinner.info('Environment is not running, starting it...'); + await gateway.startGlobal(spinner); + await environment.start(envSlug, spinner); } // Compose wp-cli command to run let snapshotCommand = false; - const command = [ - 'wp', - 'snapshots', - ]; - for ( let i = 0; i < process.argv.length; i++ ) { - if ( process.argv[i].toLowerCase() === 'wpsnapshots' || process.argv[i].toLowerCase() === 'snapshots' ) { + const command = ['wp', 'snapshots']; + for (let i = 0; i < process.argv.length; i++) { + if ( + process.argv[i].toLowerCase() === 'wpsnapshots' || + process.argv[i].toLowerCase() === 'snapshots' + ) { snapshotCommand = true; continue; } - if ( snapshotCommand ) { - command.push( process.argv[i] ); + if (snapshotCommand) { + command.push(process.argv[i]); } } @@ -62,11 +64,11 @@ exports.handler = makeCommand( async function( { env, verbose } ) { const ttyFlag = process.stdin.isTTY ? '' : ' -T'; // Run the command - execSync( `docker-compose exec${ ttyFlag } phpfpm ${ shellEscape( command ) }`, { + execSync(`docker-compose exec${ttyFlag} phpfpm ${shellEscape(command)}`, { stdio: 'inherit', - cwd: envPath - } ); + cwd: envPath, + }); } catch { // do nothing } -} ); +}); diff --git a/src/config/elasticsearch/elasticsearch.yml b/src/config/elasticsearch/elasticsearch.yml index 16cc0de7..73151f7e 100755 --- a/src/config/elasticsearch/elasticsearch.yml +++ b/src/config/elasticsearch/elasticsearch.yml @@ -1,6 +1,6 @@ http.host: 0.0.0.0 -http.cors.enabled : true -http.cors.allow-origin : "*" -http.cors.allow-methods : OPTIONS, HEAD, GET, POST, PUT, DELETE -http.cors.allow-headers : X-Requested-With,X-Auth-Token,Content-Type, Content-Length +http.cors.enabled: true +http.cors.allow-origin: '*' +http.cors.allow-methods: OPTIONS, HEAD, GET, POST, PUT, DELETE +http.cors.allow-headers: X-Requested-With,X-Auth-Token,Content-Type, Content-Length diff --git a/src/configure.js b/src/configure.js index 8229d998..5813bb8d 100644 --- a/src/configure.js +++ b/src/configure.js @@ -1,143 +1,140 @@ -const os = require( 'os' ); -const path = require( 'path' ); +const os = require('os'); +const path = require('path'); -const chalk = require( 'chalk' ); -const fsExtra = require( 'fs-extra' ); +const chalk = require('chalk'); +const fsExtra = require('fs-extra'); // Tracks current config let config = null; function getConfigDirectory() { - return path.join( os.homedir(), '.wplocaldocker' ); + return path.join(os.homedir(), '.wplocaldocker'); } function getConfigFilePath() { - return path.join( getConfigDirectory(), 'config.json' ); + return path.join(getConfigDirectory(), 'config.json'); } function getGlobalDirectory() { - return path.join( getConfigDirectory(), 'global' ); + return path.join(getConfigDirectory(), 'global'); } -async function getSslCertsDirectory( create = true ) { - const dir = path.join( getGlobalDirectory(), 'ssl-certs' ); +async function getSslCertsDirectory(create = true) { + const dir = path.join(getGlobalDirectory(), 'ssl-certs'); - if ( create ) { - await fsExtra.ensureDir( dir, { mode: 0o755 } ); + if (create) { + await fsExtra.ensureDir(dir, { mode: 0o755 }); } return dir; } async function checkIfConfigured() { - const exists = await fsExtra.pathExists( getConfigFilePath() ); + const exists = await fsExtra.pathExists(getConfigFilePath()); return exists; } async function write() { // Make sure we have our config directory present - await fsExtra.ensureDir( getConfigDirectory(), { mode: 0o755 } ); - await fsExtra.writeJson( getConfigFilePath(), config ); + await fsExtra.ensureDir(getConfigDirectory(), { mode: 0o755 }); + await fsExtra.writeJson(getConfigFilePath(), config); } async function read() { let readConfig = {}; - const exists = await fsExtra.pathExists( getConfigFilePath() ); - if ( exists ) { - readConfig = await fsExtra.readJson( getConfigFilePath() ); + const exists = await fsExtra.pathExists(getConfigFilePath()); + if (exists) { + readConfig = await fsExtra.readJson(getConfigFilePath()); } - config = Object.assign( {}, readConfig ); + config = Object.assign({}, readConfig); } -async function get( key ) { +async function get(key) { const defaults = getDefaults(); - if ( config === null ) { + if (config === null) { await read(); } - return ( typeof config[ key ] === 'undefined' ) ? defaults[ key ] : config[ key ]; + return typeof config[key] === 'undefined' ? defaults[key] : config[key]; } -async function set( key, value ) { - if ( config === null ) { +async function set(key, value) { + if (config === null) { await read(); } - config[ key ] = value; + config[key] = value; await write(); } function getDefaults() { return { - sitesPath: path.join( os.homedir(), 'wp-local-docker-sites' ), - snapshotsPath: path.join( os.homedir(), '.wpsnapshots' ), + sitesPath: path.join(os.homedir(), 'wp-local-docker-sites'), + snapshotsPath: path.join(os.homedir(), '.wpsnapshots'), manageHosts: true, - overwriteGlobal: true + overwriteGlobal: true, }; } async function configureDefaults() { - await configure( getDefaults() ); + await configure(getDefaults()); } -async function configure( configuration ) { - const sitesPath = path.resolve( configuration.sitesPath ); - const snapshotsPath = path.resolve( configuration.snapshotsPath ); +async function configure(configuration) { + const sitesPath = path.resolve(configuration.sitesPath); + const snapshotsPath = path.resolve(configuration.snapshotsPath); const globalServicesPath = getGlobalDirectory(); - if ( configuration.overwriteGlobal ) { + if (configuration.overwriteGlobal) { try { - const localGlobalPath = path.join( - path.dirname( require.main.filename ), - 'global', - ); - - await fsExtra.ensureDir( globalServicesPath, { mode: 0o755 } ); - await fsExtra.copy( localGlobalPath, globalServicesPath ); - } catch ( ex ) { - console.error( ex ); - console.error( 'Error: Unable to copy global services definition!' ); - process.exit( 1 ); + const localGlobalPath = path.join(path.dirname(require.main.filename), 'global'); + + await fsExtra.ensureDir(globalServicesPath, { mode: 0o755 }); + await fsExtra.copy(localGlobalPath, globalServicesPath); + } catch (ex) { + console.error(ex); + console.error('Error: Unable to copy global services definition!'); + process.exit(1); } } // Attempt to create the sites directory try { - await fsExtra.ensureDir( sitesPath, { mode: 0o755 } ); - } catch ( ex ) { - console.error( 'Error: Could not create directory for environments!' ); - process.exit( 1 ); + await fsExtra.ensureDir(sitesPath, { mode: 0o755 }); + } catch (ex) { + console.error('Error: Could not create directory for environments!'); + process.exit(1); } // Make sure we can write to the sites directory try { - const testfile = path.join( sitesPath, 'testfile' ); - await fsExtra.ensureFile( testfile ); - await fsExtra.remove( testfile ); - } catch ( ex ) { - console.error( 'Error: The environment directory is not writable' ); - process.exit( 1 ); + const testfile = path.join(sitesPath, 'testfile'); + await fsExtra.ensureFile(testfile); + await fsExtra.remove(testfile); + } catch (ex) { + console.error('Error: The environment directory is not writable'); + process.exit(1); } // Make sure we can write to the snapshots try { - const testfile = path.join( snapshotsPath, 'testfile' ); - await fsExtra.ensureFile( testfile ); - await fsExtra.remove( testfile ); - } catch ( ex ) { - console.error( 'Error: The snapshots directory is not writable' ); - process.exit( 1 ); + const testfile = path.join(snapshotsPath, 'testfile'); + await fsExtra.ensureFile(testfile); + await fsExtra.remove(testfile); + } catch (ex) { + console.error('Error: The snapshots directory is not writable'); + process.exit(1); } - await set( 'sitesPath', sitesPath ); - await set( 'snapshotsPath', snapshotsPath ); - await set( 'manageHosts', configuration.manageHosts ); + await set('sitesPath', sitesPath); + await set('snapshotsPath', snapshotsPath); + await set('manageHosts', configuration.manageHosts); - console.log( chalk.green( 'Successfully Configured WP Local Docker!' ) ); + console.log(chalk.green('Successfully Configured WP Local Docker!')); } /** @@ -147,27 +144,27 @@ async function configure( configuration ) { * @param {string} curConfig content of the existing config file * @return {string} New content for the config file */ -function createProxyConfig( proxy, curConfig ) { +function createProxyConfig(proxy, curConfig) { const proxyMarkup = []; // the number of spaces in the proxy markpu is intended - proxyMarkup.push( 'location @production {' ); - proxyMarkup.push( ' resolver 8.8.8.8;' ); - proxyMarkup.push( ' proxy_ssl_server_name on;' ); - proxyMarkup.push( ` proxy_pass ${ proxy }/$uri;` ); - proxyMarkup.push( '}' ); + proxyMarkup.push('location @production {'); + proxyMarkup.push(' resolver 8.8.8.8;'); + proxyMarkup.push(' proxy_ssl_server_name on;'); + proxyMarkup.push(` proxy_pass ${proxy}/$uri;`); + proxyMarkup.push('}'); const proxyMapObj = { '#{TRY_PROXY}': 'try_files $uri @production;', - '#{PROXY_URL}': proxyMarkup.join( os.EOL ), + '#{PROXY_URL}': proxyMarkup.join(os.EOL), }; - const re = new RegExp( Object.keys( proxyMapObj ).join( '|' ), 'gi' ); - const newConfig = curConfig.replace( re, function( matched ) { + const re = new RegExp(Object.keys(proxyMapObj).join('|'), 'gi'); + const newConfig = curConfig.replace(re, function (matched) { return proxyMapObj[matched]; - } ); + }); - return curConfig.replace( curConfig, newConfig ); + return curConfig.replace(curConfig, newConfig); } module.exports = { diff --git a/src/database.js b/src/database.js index a0b71b7f..0e468272 100644 --- a/src/database.js +++ b/src/database.js @@ -1,61 +1,64 @@ -const mysql = require( 'mysql' ); +const mysql = require('mysql'); -const getConnection = function() { - const connection = mysql.createConnection( { +const getConnection = function () { + const connection = mysql.createConnection({ host: '127.0.0.1', user: 'root', password: 'password', - } ); + }); return connection; }; -const create = async function( dbname ) { +const create = async function (dbname) { const connection = getConnection(); - await new Promise( ( resolve, reject ) => { - connection.query( `CREATE DATABASE IF NOT EXISTS \`${ dbname }\`;`, function ( err ) { + await new Promise((resolve, reject) => { + connection.query(`CREATE DATABASE IF NOT EXISTS \`${dbname}\`;`, function (err) { connection.destroy(); - if ( err ) { - reject( Error( err ) ); + if (err) { + reject(Error(err)); } resolve(); - } ); - } ); + }); + }); }; -const deleteDatabase = async function( dbname ) { +const deleteDatabase = async function (dbname) { const connection = getConnection(); - await new Promise( ( resolve, reject ) => { - connection.query( `DROP DATABASE IF EXISTS \`${ dbname }\`;`, function( err ) { + await new Promise((resolve, reject) => { + connection.query(`DROP DATABASE IF EXISTS \`${dbname}\`;`, function (err) { connection.destroy(); - if ( err ) { - reject( Error( err ) ); + if (err) { + reject(Error(err)); } resolve(); - } ); - } ); + }); + }); }; -const assignPrivs = async function ( dbname ) { +const assignPrivs = async function (dbname) { const connection = getConnection(); - await new Promise( ( resolve, reject ) => { - connection.query( `GRANT ALL PRIVILEGES ON \`${ dbname }\`.* TO 'wordpress'@'%' IDENTIFIED BY 'password';`, function( err ) { - connection.destroy(); + await new Promise((resolve, reject) => { + connection.query( + `GRANT ALL PRIVILEGES ON \`${dbname}\`.* TO 'wordpress'@'%' IDENTIFIED BY 'password';`, + function (err) { + connection.destroy(); - if ( err ) { - reject( Error( err ) ); - } + if (err) { + reject(Error(err)); + } - resolve(); - } ); - } ); + resolve(); + }, + ); + }); }; module.exports = { create, deleteDatabase, assignPrivs }; diff --git a/src/env-utils.js b/src/env-utils.js index 39755c13..e28fc75d 100644 --- a/src/env-utils.js +++ b/src/env-utils.js @@ -18,36 +18,36 @@ * envPath Path within `sitesPath` for the given environment */ -const path = require( 'path' ); - -const slugify = require( '@sindresorhus/slugify' ); -const asyncro = require( 'asyncro' ); -const fs = require( 'fs-extra' ); -const chalk = require( 'chalk' ); -const inquirer = require( 'inquirer' ); - -const config = require( './configure' ); -const helper = require( './helpers' ); -const { readYaml } = require( './utils/yaml' ); -const endOfLifePhpVersions = require( './utils/eol-php-versions' ); - -const rootPath = path.dirname( __dirname ); -const srcPath = path.join( rootPath, 'src' ); -const globalPath = path.join( rootPath, 'global' ); +const path = require('path'); + +const slugify = require('@sindresorhus/slugify'); +const asyncro = require('asyncro'); +const fs = require('fs-extra'); +const chalk = require('chalk'); +const inquirer = require('inquirer'); + +const config = require('./configure'); +const helper = require('./helpers'); +const { readYaml } = require('./utils/yaml'); +const endOfLifePhpVersions = require('./utils/eol-php-versions'); + +const rootPath = path.dirname(__dirname); +const srcPath = path.join(rootPath, 'src'); +const globalPath = path.join(rootPath, 'global'); const cacheVolume = 'wplocaldockerCache'; const CONFIG_FILENAME = '.config.json'; async function sitesPath() { - return await config.get( 'sitesPath' ); + return await config.get('sitesPath'); } -function envSlug( env ) { - return slugify( env ); +function envSlug(env) { + return slugify(env); } -async function envPath( env ) { - const envPath = path.join( await sitesPath(), envSlug( env ) ); +async function envPath(env) { + const envPath = path.join(await sitesPath(), envSlug(env)); return envPath; } @@ -56,47 +56,47 @@ async function parseEnvFromCWD() { try { cwd = process.cwd(); - } catch ( e ) { + } catch (e) { return false; } const sitesPathValue = await sitesPath(); - if ( cwd.indexOf( sitesPathValue ) === -1 || cwd === sitesPathValue ) { + if (cwd.indexOf(sitesPathValue) === -1 || cwd === sitesPathValue) { return false; } // Strip the base sitepath from the path - cwd = cwd.replace( sitesPathValue, '' ).replace( /^\//i, '' ); + cwd = cwd.replace(sitesPathValue, '').replace(/^\//i, ''); // First segment is now the envSlug, get rid of the rest - cwd = cwd.split( '/' )[0]; + cwd = cwd.split('/')[0]; // Make sure that a .config.json file exists here - const configFile = path.isAbsolute( cwd ) - ? path.join( cwd, CONFIG_FILENAME ) - : path.join( sitesPathValue, cwd, CONFIG_FILENAME ); + const configFile = path.isAbsolute(cwd) + ? path.join(cwd, CONFIG_FILENAME) + : path.join(sitesPathValue, cwd, CONFIG_FILENAME); - if ( ! fs.existsSync( configFile ) ) { + if (!fs.existsSync(configFile)) { return false; } return cwd; } -async function resolveEnvironment( env ) { - let envName = ( env || '' ).trim(); - if ( ! envName ) { +async function resolveEnvironment(env) { + let envName = (env || '').trim(); + if (!envName) { envName = await parseEnvFromCWD(); } - if ( envName ) { - const root = await envPath( envName ); - const dockerComposeExists = await fs.pathExists( path.join( root, 'docker-compose.yml' ) ); - if ( ! dockerComposeExists ) { + if (envName) { + const root = await envPath(envName); + const dockerComposeExists = await fs.pathExists(path.join(root, 'docker-compose.yml')); + if (!dockerComposeExists) { envName = false; } } - if ( ! envName ) { + if (!envName) { envName = await promptEnv(); } @@ -105,38 +105,38 @@ async function resolveEnvironment( env ) { async function getAllEnvironments() { const sitePath = await sitesPath(); - let dirContent = await fs.readdir( sitePath ); + let dirContent = await fs.readdir(sitePath); // Make into full path - dirContent = await asyncro.map( dirContent, async item => { - return path.join( sitePath, item ); - } ); + dirContent = await asyncro.map(dirContent, async (item) => { + return path.join(sitePath, item); + }); // Filter any that aren't directories - dirContent = await asyncro.filter( dirContent, async item => { - const stat = await fs.stat( item ); + dirContent = await asyncro.filter(dirContent, async (item) => { + const stat = await fs.stat(item); return stat.isDirectory(); - } ); + }); // Filter any that don't have the .config.json file (which indicates its probably not a WP Docker Environment) - dirContent = await asyncro.filter( dirContent, async item => { - const configFile = path.join( item, CONFIG_FILENAME ); + dirContent = await asyncro.filter(dirContent, async (item) => { + const configFile = path.join(item, CONFIG_FILENAME); - return await fs.pathExists( configFile ); - } ); + return await fs.pathExists(configFile); + }); // Back to just the basename - dirContent = await asyncro.map( dirContent, async item => { - return path.basename( item ); - } ); + dirContent = await asyncro.map(dirContent, async (item) => { + return path.basename(item); + }); return dirContent; } async function getSnapshotsPath() { // Ensure that the wpsnapshots folder is created and owned by the current user versus letting docker create it so we can enforce proper ownership later - const wpsnapshotsDir = await config.get( 'snapshotsPath' ); - await fs.ensureDir( wpsnapshotsDir ); + const wpsnapshotsDir = await config.get('snapshotsPath'); + await fs.ensureDir(wpsnapshotsDir); return wpsnapshotsDir; } @@ -149,11 +149,11 @@ async function promptEnv() { type: 'list', message: 'What environment would you like to use?', choices: environments, - } + }, ]; - console.log( chalk.bold.white( 'Unable to determine environment from current directory' ) ); - const answers = await inquirer.prompt( questions ); + console.log(chalk.bold.white('Unable to determine environment from current directory')); + const answers = await inquirer.prompt(questions); return answers.envSlug; } @@ -161,53 +161,53 @@ async function promptEnv() { async function parseOrPromptEnv() { let envSlug = await parseEnvFromCWD(); - if ( envSlug === false ) { + if (envSlug === false) { envSlug = await promptEnv(); } return envSlug; } -function saveEnvConfig( envPath, config ) { - return fs.writeJSON( path.join( envPath, CONFIG_FILENAME ), config ); +function saveEnvConfig(envPath, config) { + return fs.writeJSON(path.join(envPath, CONFIG_FILENAME), config); } -async function getEnvConfig( envPath, key = '', defaults = null ) { +async function getEnvConfig(envPath, key = '', defaults = null) { try { - const envConfig = await fs.readJson( path.join( envPath, CONFIG_FILENAME ) ); - if ( key ) { - return typeof envConfig === 'object' ? envConfig[ key ] : defaults; + const envConfig = await fs.readJson(path.join(envPath, CONFIG_FILENAME)); + if (key) { + return typeof envConfig === 'object' ? envConfig[key] : defaults; } return envConfig; - } catch ( err ) { + } catch (err) { // do nothing. } return false; } -function getEnvHosts( envPath ) { - return getEnvConfig( envPath, 'envHosts', [] ); +function getEnvHosts(envPath) { + return getEnvConfig(envPath, 'envHosts', []); } -async function getPathOrError( env, spinner ) { - if ( env === false || undefined === env || env.trim().length === 0 ) { +async function getPathOrError(env, spinner) { + if (env === false || undefined === env || env.trim().length === 0) { env = await promptEnv(); } - if ( ! spinner ) { - console.log( `Locating project files for ${ env }` ); + if (!spinner) { + console.log(`Locating project files for ${env}`); } - const _envPath = await envPath( env ); - const exists = await fs.pathExists( _envPath ); - if ( ! exists ) { - if ( spinner ) { - throw new Error( `Cannot find ${ env } environment!` ); + const _envPath = await envPath(env); + const exists = await fs.pathExists(_envPath); + if (!exists) { + if (spinner) { + throw new Error(`Cannot find ${env} environment!`); } else { - console.error( `Cannot find ${ env } environment!` ); - process.exit( 1 ); + console.error(`Cannot find ${env} environment!`); + process.exit(1); } } @@ -220,14 +220,14 @@ async function getPathOrError( env, spinner ) { * @param {string} value The user entered hostname * @return string The formatted default proxy URL */ -function createDefaultProxy( value ) { - let proxyUrl = `http://${ helper.removeEndSlashes( value ) }`; - const proxyUrlTLD = proxyUrl.lastIndexOf( '.' ); +function createDefaultProxy(value) { + let proxyUrl = `http://${helper.removeEndSlashes(value)}`; + const proxyUrlTLD = proxyUrl.lastIndexOf('.'); - if ( proxyUrlTLD === -1 ) { - proxyUrl = `${ proxyUrl }.com`; + if (proxyUrlTLD === -1) { + proxyUrl = `${proxyUrl}.com`; } else { - proxyUrl = `${ proxyUrl.substring( 0, proxyUrlTLD + 1 ) }com`; + proxyUrl = `${proxyUrl.substring(0, proxyUrlTLD + 1)}com`; } return proxyUrl; @@ -239,11 +239,11 @@ function createDefaultProxy( value ) { * @param {string} envPath path to the environemt * @return string php version */ -async function getEnvPhpVersion( envPath ) { - const dockerCompose = path.join( envPath, 'docker-compose.yml' ); - const yaml = readYaml( dockerCompose ); - const phpVersion = yaml.services.phpfpm.image.split( ':' ).pop(); - return phpVersion.split( '-' ).shift(); +async function getEnvPhpVersion(envPath) { + const dockerCompose = path.join(envPath, 'docker-compose.yml'); + const yaml = readYaml(dockerCompose); + const phpVersion = yaml.services.phpfpm.image.split(':').pop(); + return phpVersion.split('-').shift(); } /** @@ -251,9 +251,11 @@ async function getEnvPhpVersion( envPath ) { * * @param {string} phpVersion the php version to compare */ -async function checkForEOLPHP( phpVersion ) { - if ( endOfLifePhpVersions.includes( phpVersion ) ) { - console.error( `${ chalk.bold.yellow( 'Warning:' ) } This environment is using an outdated version of PHP. Please update as soon as possible.` ); +async function checkForEOLPHP(phpVersion) { + if (endOfLifePhpVersions.includes(phpVersion)) { + console.error( + `${chalk.bold.yellow('Warning:')} This environment is using an outdated version of PHP. Please update as soon as possible.`, + ); } } diff --git a/src/environment.js b/src/environment.js index 8adb6ef2..bd61e160 100644 --- a/src/environment.js +++ b/src/environment.js @@ -1,90 +1,90 @@ -const path = require( 'path' ); +const path = require('path'); -const fsExtra = require( 'fs-extra' ); -const inquirer = require( 'inquirer' ); -const sudo = require( '@vscode/sudo-prompt' ); -const chalk = require( 'chalk' ); -const which = require( 'which' ); +const fsExtra = require('fs-extra'); +const inquirer = require('inquirer'); +const sudo = require('@vscode/sudo-prompt'); +const chalk = require('chalk'); +const which = require('which'); -const config = require( './configure' ); -const promptValidators = require( './prompt-validators' ); -const database = require( './database' ); -const envUtils = require( './env-utils' ); -const gateway = require( './gateway' ); -const compose = require( './utils/docker-compose' ); +const config = require('./configure'); +const promptValidators = require('./prompt-validators'); +const database = require('./database'); +const envUtils = require('./env-utils'); +const gateway = require('./gateway'); +const compose = require('./utils/docker-compose'); -async function start( env, spinner, pull ) { - const envPath = await envUtils.getPathOrError( env, spinner ); - const envSlug = envUtils.envSlug( env ); +async function start(env, spinner, pull) { + const envPath = await envUtils.getPathOrError(env, spinner); + const envSlug = envUtils.envSlug(env); - await gateway.startGlobal( spinner, pull ); + await gateway.startGlobal(spinner, pull); const composeArgs = { cwd: envPath, log: !spinner, }; - if ( pull ) { - if ( spinner ) { - spinner.start( `Pulling latest images for ${ chalk.cyan( envSlug ) }...` ); + if (pull) { + if (spinner) { + spinner.start(`Pulling latest images for ${chalk.cyan(envSlug)}...`); } else { - console.log( 'Pulling latest images for containers' ); + console.log('Pulling latest images for containers'); } - await compose.pullAll( composeArgs ); + await compose.pullAll(composeArgs); - if ( spinner ) { - spinner.succeed( `${ chalk.cyan( envSlug ) } images are up-to-date...` ); + if (spinner) { + spinner.succeed(`${chalk.cyan(envSlug)} images are up-to-date...`); } } - if ( spinner ) { - spinner.start( `Starting docker containers for ${ chalk.cyan( envSlug ) }...` ); + if (spinner) { + spinner.start(`Starting docker containers for ${chalk.cyan(envSlug)}...`); } else { - console.log( `Starting docker containers for ${ envSlug }` ); + console.log(`Starting docker containers for ${envSlug}`); } - await compose.upAll( composeArgs ); + await compose.upAll(composeArgs); - if ( spinner ) { - spinner.succeed( `${ chalk.cyan( envSlug ) } is started...` ); + if (spinner) { + spinner.succeed(`${chalk.cyan(envSlug)} is started...`); } else { console.log(); } } -async function stop( env, spinner ) { - const envPath = await envUtils.getPathOrError( env, spinner ); - const envSlug = envUtils.envSlug( env ); +async function stop(env, spinner) { + const envPath = await envUtils.getPathOrError(env, spinner); + const envSlug = envUtils.envSlug(env); - if ( spinner ) { - spinner.start( `Stopping docker containers for ${ chalk.cyan( envSlug ) }...` ); + if (spinner) { + spinner.start(`Stopping docker containers for ${chalk.cyan(envSlug)}...`); } else { - console.log( `Stopping docker containers for ${ envSlug }` ); + console.log(`Stopping docker containers for ${envSlug}`); } - await compose.down( { + await compose.down({ cwd: envPath, log: !spinner, - } ); + }); - if ( spinner ) { - spinner.succeed( `${ chalk.cyan( envSlug ) } is stopped...` ); + if (spinner) { + spinner.succeed(`${chalk.cyan(envSlug)} is stopped...`); } else { console.log(); } } -async function restart( env, spinner ) { - const envPath = await envUtils.getPathOrError( env, spinner ); - const envSlug = envUtils.envSlug( env ); +async function restart(env, spinner) { + const envPath = await envUtils.getPathOrError(env, spinner); + const envSlug = envUtils.envSlug(env); - await gateway.startGlobal( spinner ); + await gateway.startGlobal(spinner); - if ( spinner ) { - spinner.start( `Restarting docker containers for ${ chalk.cyan( envSlug ) }...` ); + if (spinner) { + spinner.start(`Restarting docker containers for ${chalk.cyan(envSlug)}...`); } else { - console.log( `Restarting docker containers for ${ envSlug }` ); + console.log(`Restarting docker containers for ${envSlug}`); } const composeArgs = { @@ -92,185 +92,197 @@ async function restart( env, spinner ) { log: !spinner, }; - const isRunning = await compose.isRunning( envPath ); - if ( isRunning ) { - if ( spinner ) { - spinner.start( `Stopping docker containers for ${ chalk.cyan( envSlug ) }...` ); + const isRunning = await compose.isRunning(envPath); + if (isRunning) { + if (spinner) { + spinner.start(`Stopping docker containers for ${chalk.cyan(envSlug)}...`); } else { - console.log( `Stopping docker containers for ${ envSlug }` ); + console.log(`Stopping docker containers for ${envSlug}`); } - await compose.down( composeArgs ); + await compose.down(composeArgs); - if ( spinner ) { - spinner.succeed( `${ chalk.cyan( envSlug ) } has stopped...` ); + if (spinner) { + spinner.succeed(`${chalk.cyan(envSlug)} has stopped...`); } } - if ( spinner ) { - spinner.start( `Starting docker containers for ${ chalk.cyan( envSlug ) }...` ); + if (spinner) { + spinner.start(`Starting docker containers for ${chalk.cyan(envSlug)}...`); } - await compose.upAll( composeArgs ); + await compose.upAll(composeArgs); - if ( spinner ) { - spinner.succeed( `${ chalk.cyan( envSlug ) } has started...` ); + if (spinner) { + spinner.succeed(`${chalk.cyan(envSlug)} has started...`); } } -async function deleteEnv( env, spinner ) { - const envSlug = envUtils.envSlug( env ); - const envPath = await envUtils.getPathOrError( env, spinner ); - const envHosts = await envUtils.getEnvHosts( envPath ); +async function deleteEnv(env, spinner) { + const envSlug = envUtils.envSlug(env); + const envPath = await envUtils.getPathOrError(env, spinner); + const envHosts = await envUtils.getEnvHosts(envPath); - const answers = await inquirer.prompt( { + const answers = await inquirer.prompt({ name: 'confirm', type: 'confirm', - message: `Are you sure you want to delete the ${ envSlug } environment`, + message: `Are you sure you want to delete the ${envSlug} environment`, validate: promptValidators.validateNotEmpty, default: false, - } ); + }); - if ( answers.confirm === false ) { + if (answers.confirm === false) { return; } - await gateway.startGlobal( spinner ); + await gateway.startGlobal(spinner); // Stop the environment, and ensure volumes are deleted with it - if ( spinner ) { - spinner.start( 'Deleting containers...' ); + if (spinner) { + spinner.start('Deleting containers...'); } else { - console.log( 'Deleting containers' ); + console.log('Deleting containers'); } try { - await compose.down( { + await compose.down({ cwd: envPath, log: !spinner, - commandOptions: [ '-v' ], - } ); - } catch ( ex ) { + commandOptions: ['-v'], + }); + } catch (ex) { // If the docker-compose file is already gone, this happens } - if ( spinner ) { - spinner.start( 'Containers are deleted...' ); + if (spinner) { + spinner.start('Containers are deleted...'); } - if ( spinner ) { - spinner.start( 'Deleting environment files...' ); + if (spinner) { + spinner.start('Deleting environment files...'); } else { - console.log( 'Deleting Files' ); + console.log('Deleting Files'); } - await fsExtra.remove( envPath ); + await fsExtra.remove(envPath); - if ( spinner ) { - spinner.succeed( 'Environment files are deleted...' ); - spinner.start( 'Deleting certificates...' ); + if (spinner) { + spinner.succeed('Environment files are deleted...'); + spinner.start('Deleting certificates...'); } else { - console.log( 'Deleting Certificates' ); + console.log('Deleting Certificates'); } - const sslDir = await config.getSslCertsDirectory( false ); - const filename = path.join( sslDir, envSlug ); - await fsExtra.remove( `${ filename }.crt` ); - await fsExtra.remove( `${ filename }.key` ); + const sslDir = await config.getSslCertsDirectory(false); + const filename = path.join(sslDir, envSlug); + await fsExtra.remove(`${filename}.crt`); + await fsExtra.remove(`${filename}.key`); - if ( spinner ) { - spinner.succeed( 'Environment certificates are deleted...' ); - spinner.start( 'Deleting database...' ); + if (spinner) { + spinner.succeed('Environment certificates are deleted...'); + spinner.start('Deleting database...'); } else { - console.log( 'Deleting Database' ); + console.log('Deleting Database'); } - await database.deleteDatabase( envSlug ); + await database.deleteDatabase(envSlug); - if ( spinner ) { - spinner.succeed( 'Database is deleted...' ); + if (spinner) { + spinner.succeed('Database is deleted...'); } - if ( await config.get( 'manageHosts' ) === true ) { + if ((await config.get('manageHosts')) === true) { try { - if ( spinner ) { - spinner.start( 'Removing host file entries...' ); + if (spinner) { + spinner.start('Removing host file entries...'); } else { - console.log( 'Removing host file entries' ); + console.log('Removing host file entries'); } const sudoOptions = { name: 'WP Docker' }; - const node = await which( 'node' ); - const hostsScript = path.join( path.resolve( __dirname, '..' ), 'hosts.js' ); - const hostsToDelete = envHosts.join( ' ' ); + const node = await which('node'); + const hostsScript = path.join(path.resolve(__dirname, '..'), 'hosts.js'); + const hostsToDelete = envHosts.join(' '); - await new Promise( resolve => { - if ( !spinner ) { - console.log( ` - Removing ${ hostsToDelete }` ); + await new Promise((resolve) => { + if (!spinner) { + console.log(` - Removing ${hostsToDelete}`); } - sudo.exec( `${ node } "${ hostsScript }" remove ${ hostsToDelete }`, sudoOptions, ( error, stdout ) => { - if ( error ) { - if ( spinner ) { - spinner.warn( 'Something went wrong deleting host file entries. There may still be remnants in /etc/hosts' ); + sudo.exec( + `${node} "${hostsScript}" remove ${hostsToDelete}`, + sudoOptions, + (error, stdout) => { + if (error) { + if (spinner) { + spinner.warn( + 'Something went wrong deleting host file entries. There may still be remnants in /etc/hosts', + ); + } else { + console.error( + `${chalk.bold.yellow('Warning: ')}Something went wrong deleting host file entries. There may still be remnants in /etc/hosts`, + ); + } } else { - console.error( `${ chalk.bold.yellow( 'Warning: ' ) }Something went wrong deleting host file entries. There may still be remnants in /etc/hosts` ); + if (spinner) { + spinner.succeed('Host file entries are deleted...'); + } else { + console.log(stdout); + } } - } else { - if ( spinner ) { - spinner.succeed( 'Host file entries are deleted...' ); - } else { - console.log( stdout ); - } - } - resolve(); - } ); - } ); - } catch ( err ) { + resolve(); + }, + ); + }); + } catch (err) { // Unfound config, etc - if ( spinner ) { - spinner.warn( 'Something went wrong deleting host file entries. There may still be remnants in /etc/hosts' ); + if (spinner) { + spinner.warn( + 'Something went wrong deleting host file entries. There may still be remnants in /etc/hosts', + ); } else { - console.error( `${ chalk.bold.yellow( 'Warning: ' ) }Something went wrong deleting host file entries. There may still be remnants in /etc/hosts` ); + console.error( + `${chalk.bold.yellow('Warning: ')}Something went wrong deleting host file entries. There may still be remnants in /etc/hosts`, + ); } } } } -async function startAll( spinner, pull ) { +async function startAll(spinner, pull) { const envs = await envUtils.getAllEnvironments(); - await gateway.startGlobal( spinner, pull ); + await gateway.startGlobal(spinner, pull); - for ( let i = 0, len = envs.length; i < len; i++ ) { - await start( envs[i], spinner, pull ); + for (let i = 0, len = envs.length; i < len; i++) { + await start(envs[i], spinner, pull); } } -async function stopAll( spinner ) { +async function stopAll(spinner) { const envs = await envUtils.getAllEnvironments(); - for ( let i = 0, len = envs.length; i < len; i++ ) { - await stop( envs[ i ], spinner ); + for (let i = 0, len = envs.length; i < len; i++) { + await stop(envs[i], spinner); } - await gateway.stopGlobal( spinner ); + await gateway.stopGlobal(spinner); } -async function restartAll( spinner ) { +async function restartAll(spinner) { const envs = await envUtils.getAllEnvironments(); - for ( let i = 0, len = envs.length; i < len; i++ ) { - await restart( envs[ i ], spinner ); + for (let i = 0, len = envs.length; i < len; i++) { + await restart(envs[i], spinner); } - await gateway.restartGlobal( spinner ); + await gateway.restartGlobal(spinner); } -async function deleteAll( spinner ) { +async function deleteAll(spinner) { const envs = await envUtils.getAllEnvironments(); - for ( let i = 0, len = envs.length; i < len; i++ ) { - await deleteEnv( envs[ i ], spinner ); + for (let i = 0, len = envs.length; i < len; i++) { + await deleteEnv(envs[i], spinner); } } diff --git a/src/gateway.js b/src/gateway.js index 17ec702f..fb0e5174 100644 --- a/src/gateway.js +++ b/src/gateway.js @@ -1,39 +1,39 @@ -const fs = require( 'fs' ); -const path = require( 'path' ); -const { EOL } = require( 'os' ); +const fs = require('fs'); +const path = require('path'); +const { EOL } = require('os'); -const nc = require( 'netcat/client' ); +const nc = require('netcat/client'); -const envUtils = require( './env-utils' ); -const config = require( './configure' ); -const makeDocker = require( './utils/make-docker' ); -const compose = require( './utils/docker-compose' ); +const envUtils = require('./env-utils'); +const config = require('./configure'); +const makeDocker = require('./utils/make-docker'); +const compose = require('./utils/docker-compose'); // Tracks if we've started global inside of this session let started = false; -async function ensureNetworkExists( docker, spinner ) { - if ( !spinner ) { - console.log( 'Ensuring global network exists' ); +async function ensureNetworkExists(docker, spinner) { + if (!spinner) { + console.log('Ensuring global network exists'); } const networkName = 'wplocaldocker'; - const network = docker.getNetwork( networkName ); - const data = await network.inspect().catch( () => false ); - if ( data ) { - if ( !spinner ) { - console.log( ' - Network exists' ); + const network = docker.getNetwork(networkName); + const data = await network.inspect().catch(() => false); + if (data) { + if (!spinner) { + console.log(' - Network exists'); } return; } - if ( !spinner ) { - console.log( ' - Creating network' ); + if (!spinner) { + console.log(' - Creating network'); } // --ip-range is only half of the subnet, so that we have a bunch of addresses in front to assign manually - await docker.createNetwork( { + await docker.createNetwork({ Name: networkName, IPAM: { Driver: 'default', @@ -45,63 +45,63 @@ async function ensureNetworkExists( docker, spinner ) { }, ], }, - } ); + }); } -async function removeNetwork( docker, spinner ) { - if ( !spinner ) { - console.log( 'Removing Global Network' ); +async function removeNetwork(docker, spinner) { + if (!spinner) { + console.log('Removing Global Network'); } - const network = docker.getNetwork( 'wplocaldocker' ); - const data = await network.inspect().catch( () => false ); - if ( !data ) { + const network = docker.getNetwork('wplocaldocker'); + const data = await network.inspect().catch(() => false); + if (!data) { return; } await network.remove(); - if ( !spinner ) { - console.log( ' - Network Removed' ); + if (!spinner) { + console.log(' - Network Removed'); } } -async function ensureCacheExists( docker, spinner ) { - if ( !spinner ) { - console.log( 'Ensuring global cache volume exists' ); +async function ensureCacheExists(docker, spinner) { + if (!spinner) { + console.log('Ensuring global cache volume exists'); } - const volume = docker.getVolume( envUtils.cacheVolume ); - const data = await volume.inspect().catch( () => false ); + const volume = docker.getVolume(envUtils.cacheVolume); + const data = await volume.inspect().catch(() => false); - if ( data ) { - if ( !spinner ) { - console.log( ' - Volume Exists' ); + if (data) { + if (!spinner) { + console.log(' - Volume Exists'); } } else { - if ( !spinner ) { - console.log( ' - Creating Volume' ); + if (!spinner) { + console.log(' - Creating Volume'); } - await docker.createVolume( { + await docker.createVolume({ Name: envUtils.cacheVolume, - } ); + }); } } -async function removeCacheVolume( docker, spinner ) { - const volume = docker.getVolume( envUtils.cacheVolume ); - const data = await volume.inspect().catch( () => false ); +async function removeCacheVolume(docker, spinner) { + const volume = docker.getVolume(envUtils.cacheVolume); + const data = await volume.inspect().catch(() => false); - if ( data ) { - if ( !spinner ) { - console.log( 'Removing cache volume' ); + if (data) { + if (!spinner) { + console.log('Removing cache volume'); } await volume.remove(); - if ( !spinner ) { - console.log( ' - Volume Removed' ); + if (!spinner) { + console.log(' - Volume Removed'); } } } @@ -114,37 +114,37 @@ async function removeCacheVolume( docker, spinner ) { * here to connect the the MariaDB port and will resolve the promise once MariaDB sends data back indicating * MariaDB is ready for work. */ -function waitForDB( spinner ) { - if ( spinner ) { - spinner.start( 'Waiting for MariaDB...' ); +function waitForDB(spinner) { + if (spinner) { + spinner.start('Waiting for MariaDB...'); } else { - console.log( 'Waiting for MariaDB...' ); + console.log('Waiting for MariaDB...'); } - return new Promise( ( resolve ) => { - const interval = setInterval( () => { + return new Promise((resolve) => { + const interval = setInterval(() => { const netcat = new nc(); - netcat.address( '127.0.0.1' ); - netcat.port( 3306 ); + netcat.address('127.0.0.1'); + netcat.port(3306); netcat.connect(); - netcat.on( 'data', function () { + netcat.on('data', function () { netcat.close(); - if ( spinner ) { - spinner.succeed( 'MariaDB has started...' ); + if (spinner) { + spinner.succeed('MariaDB has started...'); } - clearInterval( interval ); + clearInterval(interval); resolve(); - } ); - }, 1000 ); - } ); + }); + }, 1000); + }); } -async function startGateway( spinner, pull ) { - let cwd = path.join( config.getConfigDirectory(), 'global' ); - if ( !fs.existsSync( cwd ) ) { +async function startGateway(spinner, pull) { + let cwd = path.join(config.getConfigDirectory(), 'global'); + if (!fs.existsSync(cwd)) { cwd = envUtils.globalPath; } @@ -153,110 +153,110 @@ async function startGateway( spinner, pull ) { log: !spinner, }; - if ( pull ) { - if ( spinner ) { - spinner.start( 'Pulling latest images for global services...' ); + if (pull) { + if (spinner) { + spinner.start('Pulling latest images for global services...'); } else { - console.log( 'Pulling latest images for global services' ); + console.log('Pulling latest images for global services'); } - await compose.pullAll( composeArgs ); + await compose.pullAll(composeArgs); - if ( spinner ) { - spinner.succeed( 'Global images are up-to-date...' ); + if (spinner) { + spinner.succeed('Global images are up-to-date...'); } } - if ( spinner ) { - spinner.start( 'Ensuring global services are running...' ); + if (spinner) { + spinner.start('Ensuring global services are running...'); } else { - console.log( 'Ensuring global services are running' ); + console.log('Ensuring global services are running'); } - await compose.upAll( composeArgs ); + await compose.upAll(composeArgs); - if ( spinner ) { - spinner.succeed( 'Global services are running...' ); + if (spinner) { + spinner.succeed('Global services are running...'); } - await waitForDB( spinner ); + await waitForDB(spinner); } -async function stopGateway( spinner ) { - if ( spinner ) { - spinner.start( 'Stopping global services...' ); +async function stopGateway(spinner) { + if (spinner) { + spinner.start('Stopping global services...'); } else { - console.log( 'Stopping global services' ); + console.log('Stopping global services'); } - await compose.down( { + await compose.down({ cwd: envUtils.globalPath, log: !spinner, - } ); + }); - if ( spinner ) { - spinner.succeed( 'Global services are stopped...' ); + if (spinner) { + spinner.succeed('Global services are stopped...'); } else { console.log(); } } -async function restartGateway( spinner ) { - if ( spinner ) { - spinner.start( 'Restarting global services...' ); +async function restartGateway(spinner) { + if (spinner) { + spinner.start('Restarting global services...'); } else { - console.log( 'Restarting global services' ); + console.log('Restarting global services'); } - await compose.restartAll( { + await compose.restartAll({ cwd: envUtils.globalPath, log: !spinner, - } ); + }); - if ( spinner ) { - spinner.succeed( 'Global services are restarted...' ); + if (spinner) { + spinner.succeed('Global services are restarted...'); } else { console.log(); } } -async function startGlobal( spinner, pull ) { - if ( started === true ) { +async function startGlobal(spinner, pull) { + if (started === true) { return; } const docker = makeDocker(); - await ensureNetworkExists( docker, spinner ); - await ensureCacheExists( docker, spinner ); - await startGateway( spinner, pull ); + await ensureNetworkExists(docker, spinner); + await ensureCacheExists(docker, spinner); + await startGateway(spinner, pull); started = true; } -async function stopGlobal( spinner ) { +async function stopGlobal(spinner) { try { const docker = makeDocker(); - await stopGateway( spinner ); - await removeNetwork( docker, spinner ); - } catch ( err ) { - process.stderr.write( err.toString() + EOL ); + await stopGateway(spinner); + await removeNetwork(docker, spinner); + } catch (err) { + process.stderr.write(err.toString() + EOL); } started = false; } -async function restartGlobal( spinner ) { +async function restartGlobal(spinner) { try { const docker = makeDocker(); - await ensureNetworkExists( docker, spinner ); - await restartGateway( spinner ); + await ensureNetworkExists(docker, spinner); + await restartGateway(spinner); started = true; - } catch ( err ) { - process.stderr.write( err.toString() + EOL ); + } catch (err) { + process.stderr.write(err.toString() + EOL); } } diff --git a/src/helpers.js b/src/helpers.js index 1d52c9d0..8852a9ec 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -7,8 +7,8 @@ * * @param string string The string to remove slashes from */ -const unleadingslashit = function ( string ) { - return string.replace( /^\/+/g, '' ); +const unleadingslashit = function (string) { + return string.replace(/^\/+/g, ''); }; /** @@ -16,8 +16,8 @@ const unleadingslashit = function ( string ) { * * @param string string The string to remove slashes from */ -const untrailingslashit = function ( string ) { - return string.replace( /\/$/, '' ); +const untrailingslashit = function (string) { + return string.replace(/\/$/, ''); }; /** @@ -25,8 +25,8 @@ const untrailingslashit = function ( string ) { * * @param string string The string to remove slashes from */ -const removeEndSlashes = function ( string ) { - return unleadingslashit( untrailingslashit( string ) ); +const removeEndSlashes = function (string) { + return unleadingslashit(untrailingslashit(string)); }; module.exports = { unleadingslashit, untrailingslashit, removeEndSlashes }; diff --git a/src/prompt-validators.js b/src/prompt-validators.js index 2beb94ae..10739c91 100644 --- a/src/prompt-validators.js +++ b/src/prompt-validators.js @@ -1,20 +1,20 @@ -const helper = require( './helpers' ); +const helper = require('./helpers'); -const validateNotEmpty = function( value ) { - return ( value.trim().length !== 0 ) ? true : 'This field is required'; +const validateNotEmpty = function (value) { + return value.trim().length !== 0 ? true : 'This field is required'; }; -const validateBool = function( value ) { - const y = new RegExp( /^y(es)?$/i ); - const n = new RegExp( /^no?$/i ); +const validateBool = function (value) { + const y = new RegExp(/^y(es)?$/i); + const n = new RegExp(/^no?$/i); - if ( typeof value !== 'string' ) { + if (typeof value !== 'string') { return value; } - if ( value.match( y ) !== null ) { + if (value.match(y) !== null) { return 'true'; - } else if ( value.match( n ) !== null ) { + } else if (value.match(n) !== null) { return 'false'; } @@ -24,14 +24,14 @@ const validateBool = function( value ) { /* Not foolproof, but should catch some more common issues with entering hostnames */ -const parseHostname = function( value ) { +const parseHostname = function (value) { // Get rid of any http(s):// prefix - value = value.replace( /^https?:\/\//i, '' ); + value = value.replace(/^https?:\/\//i, ''); // Get rid of any spaces - value = value.replace( /\s/i, '' ); + value = value.replace(/\s/i, ''); - const parts = value.split( '/' ); + const parts = value.split('/'); const hostname = parts[0]; @@ -44,14 +44,14 @@ const parseHostname = function( value ) { * @param string value Proxy URL to check against * @return string The validated/modified proxy URL */ -const parseProxyUrl = function( value ) { +const parseProxyUrl = function (value) { const re = /^https?:\/\//i; - if ( value.length > 3 && ! re.test( value ) ) { - value = `http://${ value }`; + if (value.length > 3 && !re.test(value)) { + value = `http://${value}`; } - return helper.removeEndSlashes( value ); + return helper.removeEndSlashes(value); }; module.exports = { validateNotEmpty, validateBool, parseHostname, parseProxyUrl }; diff --git a/src/utils/display-error.js b/src/utils/display-error.js index aea88d75..d0e9fbc1 100644 --- a/src/utils/display-error.js +++ b/src/utils/display-error.js @@ -1,13 +1,13 @@ -const { EOL } = require( 'os' ); +const { EOL } = require('os'); -const chalk = require( 'chalk' ); +const chalk = require('chalk'); -module.exports = function displayError( message = 'Unexpected error', exitCode = 1 ) { - process.stderr.write( EOL ); - process.stderr.write( chalk.bgHex( '#a70334' )( ' Error ' ) ); - process.stderr.write( chalk.gray( ': ' ) ); - process.stderr.write( chalk.whiteBright( message ) ); - process.stderr.write( EOL ); - process.stderr.write( EOL ); - process.exit( exitCode ); +module.exports = function displayError(message = 'Unexpected error', exitCode = 1) { + process.stderr.write(EOL); + process.stderr.write(chalk.bgHex('#a70334')(' Error ')); + process.stderr.write(chalk.gray(': ')); + process.stderr.write(chalk.whiteBright(message)); + process.stderr.write(EOL); + process.stderr.write(EOL); + process.exit(exitCode); }; diff --git a/src/utils/docker-compose.js b/src/utils/docker-compose.js index 9bfc8399..fa9fc7d3 100644 --- a/src/utils/docker-compose.js +++ b/src/utils/docker-compose.js @@ -1,35 +1,34 @@ -const compose = require( 'docker-compose' ); +const compose = require('docker-compose'); -function interpretComposerResults( { out, err, exitCode } ) { - if ( exitCode ) { - throw new Error( err ); +function interpretComposerResults({ out, err, exitCode }) { + if (exitCode) { + throw new Error(err); } return out; } -function makeProxyFunction( fn ) { - return async function( ...args ) { - const results = await compose[ fn ]( ...args ); - return interpretComposerResults( results ); +function makeProxyFunction(fn) { + return async function (...args) { + const results = await compose[fn](...args); + return interpretComposerResults(results); }; } -exports.down = makeProxyFunction( 'down' ); -exports.exec = makeProxyFunction( 'exec' ); -exports.logs = makeProxyFunction( 'logs' ); -exports.ps = makeProxyFunction( 'ps' ); -exports.pullAll = makeProxyFunction( 'pullAll' ); -exports.restartAll = makeProxyFunction( 'restartAll' ); -exports.run = makeProxyFunction( 'run' ); -exports.upAll = makeProxyFunction( 'upAll' ); +exports.down = makeProxyFunction('down'); +exports.exec = makeProxyFunction('exec'); +exports.logs = makeProxyFunction('logs'); +exports.ps = makeProxyFunction('ps'); +exports.pullAll = makeProxyFunction('pullAll'); +exports.restartAll = makeProxyFunction('restartAll'); +exports.run = makeProxyFunction('run'); +exports.upAll = makeProxyFunction('upAll'); -exports.isRunning = async function( cwd ) { +exports.isRunning = async function (cwd) { try { - await compose.port( 'nginx', 80, { cwd } ); + await compose.port('nginx', 80, { cwd }); return true; - } catch { - } + } catch {} return false; }; diff --git a/src/utils/eol-php-versions.js b/src/utils/eol-php-versions.js index 00598408..04e300f3 100644 --- a/src/utils/eol-php-versions.js +++ b/src/utils/eol-php-versions.js @@ -1,7 +1 @@ -module.exports = [ - '5.6', - '7.0', - '7.1', - '7.2', - '7.3', -]; \ No newline at end of file +module.exports = ['5.6', '7.0', '7.1', '7.2', '7.3']; diff --git a/src/utils/make-boxen.js b/src/utils/make-boxen.js index 59dee6e9..41545567 100644 --- a/src/utils/make-boxen.js +++ b/src/utils/make-boxen.js @@ -1,10 +1,11 @@ -const boxen = require( 'boxen' ); +const boxen = require('boxen'); -module.exports = function makeBoxen( args = {} ) { - return ( message ) => boxen( message.trim(), { - padding: 2, - align: 'left', - borderColor: 'magentaBright', - ...args, - } ); +module.exports = function makeBoxen(args = {}) { + return (message) => + boxen(message.trim(), { + padding: 2, + align: 'left', + borderColor: 'magentaBright', + ...args, + }); }; diff --git a/src/utils/make-command.js b/src/utils/make-command.js index 65c54dc5..2f96cf31 100644 --- a/src/utils/make-command.js +++ b/src/utils/make-command.js @@ -1,24 +1,22 @@ -const makeDocker = require( './make-docker' ); -const displayError = require( './display-error' ); +const makeDocker = require('./make-docker'); +const displayError = require('./display-error'); -function makeCommand( options, command ) { +function makeCommand(options, command) { const cmd = typeof options === 'function' ? options : command; - const { - checkDocker = true, - } = typeof options === 'object' ? options : {}; + const { checkDocker = true } = typeof options === 'object' ? options : {}; - return async ( ...params ) => { - if ( checkDocker === true ) { + return async (...params) => { + if (checkDocker === true) { const docker = makeDocker(); - const ping = await docker.ping().catch( () => false ); - if ( ! ping ) { - displayError( 'Docker is not running...' ); + const ping = await docker.ping().catch(() => false); + if (!ping) { + displayError('Docker is not running...'); } } - return cmd( ...params ).catch( ( err ) => { - displayError( err.message || err.err, err.exitCode ); - } ); + return cmd(...params).catch((err) => { + displayError(err.message || err.err, err.exitCode); + }); }; } diff --git a/src/utils/make-docker.js b/src/utils/make-docker.js index 106109b1..cfa8c083 100644 --- a/src/utils/make-docker.js +++ b/src/utils/make-docker.js @@ -1,7 +1,7 @@ -const Docker = require( 'dockerode' ); +const Docker = require('dockerode'); -module.exports = function makeDocker( args = {} ) { - return new Docker( { +module.exports = function makeDocker(args = {}) { + return new Docker({ ...args, - } ); + }); }; diff --git a/src/utils/make-link.js b/src/utils/make-link.js index 8e53b2d1..56e00b39 100644 --- a/src/utils/make-link.js +++ b/src/utils/make-link.js @@ -1,21 +1,21 @@ -const chalk = require( 'chalk' ); -const terminalLink = require( 'terminal-link' ); -const stripAnsi = require( 'strip-ansi' ); +const chalk = require('chalk'); +const terminalLink = require('terminal-link'); +const stripAnsi = require('strip-ansi'); -function makeLink( text, link ) { - if ( ! terminalLink.isSupported ) { - return link !== stripAnsi( text ) ? `${ text } (${ link })` : link; +function makeLink(text, link) { + if (!terminalLink.isSupported) { + return link !== stripAnsi(text) ? `${text} (${link})` : link; } - return terminalLink( text, link ); + return terminalLink(text, link); } -function replaceLinks( text, links ) { +function replaceLinks(text, links) { let output = text; - Object.keys( links ).forEach( ( label ) => { - output = output.replace( label, makeLink( chalk.cyanBright( label ), links[ label ] ) ); - } ); + Object.keys(links).forEach((label) => { + output = output.replace(label, makeLink(chalk.cyanBright(label), links[label])); + }); return output; } diff --git a/src/utils/make-markdown.js b/src/utils/make-markdown.js index d8c4913b..b4997205 100644 --- a/src/utils/make-markdown.js +++ b/src/utils/make-markdown.js @@ -1,32 +1,31 @@ -const { EOL } = require( 'os' ); +const { EOL } = require('os'); -const MarkdownIt = require( 'markdown-it' ); -const chalk = require( 'chalk' ); +const MarkdownIt = require('markdown-it'); +const chalk = require('chalk'); -const { makeLink } = require( './make-link' ); +const { makeLink } = require('./make-link'); -module.exports = function() { - return ( markdown ) => { +module.exports = function () { + return (markdown) => { const md = new MarkdownIt(); - md.renderer.render = function( tokens ) { + md.renderer.render = function (tokens) { const renderer = new Renderer(); - return renderer.render( tokens ); + return renderer.render(tokens); }; - return md.render( markdown ) + EOL; + return md.render(markdown) + EOL; }; }; class Renderer { - constructor() { this.indents = []; this.prevTag = ''; } expandIndent() { - this.indents.push( ' ' ); + this.indents.push(' '); } shrinkIndent() { @@ -34,37 +33,35 @@ class Renderer { } getIndent() { - return this.indents.join( '' ); + return this.indents.join(''); } - getSpacer( token ) { - const blockTags = [ 'p', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ]; + getSpacer(token) { + const blockTags = ['p', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']; - if ( token.level > 0 && ( token.tag === 'ul' || token.tag === 'ol' ) ) { + if (token.level > 0 && (token.tag === 'ul' || token.tag === 'ol')) { return ''; } - return blockTags.includes( this.prevTag ) && blockTags.includes( token.tag ) - ? EOL - : ''; + return blockTags.includes(this.prevTag) && blockTags.includes(token.tag) ? EOL : ''; } - render( tokens ) { + render(tokens) { const blocks = []; const result = []; const ol = []; - for ( let i = 0; i < tokens.length; i++ ) { + for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; - const spacer = this.getSpacer( token ); - if ( spacer ) { - result.push( spacer ); + const spacer = this.getSpacer(token); + if (spacer) { + result.push(spacer); } - switch ( token.type ) { + switch (token.type) { case 'ordered_list_open': - ol.push( 0 ); + ol.push(0); this.expandIndent(); break; case 'ordered_list_close': @@ -78,15 +75,15 @@ class Renderer { this.shrinkIndent(); break; case 'list_item_open': { - blocks.push( token ); - result.push( ''.padEnd( token.level - 1, ' ' ) ); - if ( token.markup === '.' ) { - if ( ol.length > 0 ) { - ol[ ol.length - 1 ] = ol[ ol.length - 1 ] + 1; - result.push( ol.join( '.' ), ') ' ); + blocks.push(token); + result.push(''.padEnd(token.level - 1, ' ')); + if (token.markup === '.') { + if (ol.length > 0) { + ol[ol.length - 1] = ol[ol.length - 1] + 1; + result.push(ol.join('.'), ') '); } } else { - result.push( '• ' ); + result.push('• '); } break; } @@ -95,7 +92,7 @@ class Renderer { case 'link_open': case 'heading_open': case 'paragraph_open': - blocks.push( token ); + blocks.push(token); break; case 'list_item_close': case 'strong_close': @@ -104,53 +101,57 @@ class Renderer { case 'heading_close': case 'paragraph_close': { const prevToken = blocks.pop(); - switch ( prevToken.tag ) { + switch (prevToken.tag) { case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': - result.push( chalk.bold.cyanBright( `${ token.markup } ` ), chalk.bold.cyanBright( result.pop() ), EOL ); + result.push( + chalk.bold.cyanBright(`${token.markup} `), + chalk.bold.cyanBright(result.pop()), + EOL, + ); break; case 'a': { - const href = ( prevToken.attrs.find( ( [ attr ] ) => attr === 'href' ) || [] )[ 1 ]; - result.push( href ? makeLink( result.pop(), href ) : result.pop() ); + const href = (prevToken.attrs.find(([attr]) => attr === 'href') || + [])[1]; + result.push(href ? makeLink(result.pop(), href) : result.pop()); break; } case 'strong': - result.push( chalk.bold( result.pop() ) ); + result.push(chalk.bold(result.pop())); break; case 'em': - result.push( chalk.italic( result.pop() ) ); + result.push(chalk.italic(result.pop())); break; case 'p': - result.push( EOL ); + result.push(EOL); break; } break; } case 'inline': - if ( Array.isArray( token.children ) && token.children.length > 0 ) { - result.push( this.render( token.children ) ); + if (Array.isArray(token.children) && token.children.length > 0) { + result.push(this.render(token.children)); } break; case 'code_inline': - result.push( chalk.yellow( token.content ) ); + result.push(chalk.yellow(token.content)); break; case 'text': - result.push( token.content ); + result.push(token.content); break; case 'softbreak': - result.push( EOL ); - result.push( this.getIndent() ); + result.push(EOL); + result.push(this.getIndent()); break; } this.prevTag = token.tag; } - return result.join( '' ).trim(); + return result.join('').trim(); } - } diff --git a/src/utils/make-spinner.js b/src/utils/make-spinner.js index b206f086..9e30bcbb 100644 --- a/src/utils/make-spinner.js +++ b/src/utils/make-spinner.js @@ -1,10 +1,10 @@ -const ora = require( 'ora' ); +const ora = require('ora'); -module.exports = function makeSpinner( args = {} ) { - return ora( { +module.exports = function makeSpinner(args = {}) { + return ora({ spinner: 'dots', color: 'white', hideCursor: true, ...args, - } ); + }); }; diff --git a/src/utils/make-table.js b/src/utils/make-table.js index b6b78776..a6d9718a 100644 --- a/src/utils/make-table.js +++ b/src/utils/make-table.js @@ -1,7 +1,7 @@ -const chalk = require( 'chalk' ); -const { table } = require( 'table' ); +const chalk = require('chalk'); +const { table } = require('table'); -module.exports = function makeTable( data ) { +module.exports = function makeTable(data) { const border = { topBody: '─', topJoin: '┬', @@ -20,18 +20,16 @@ module.exports = function makeTable( data ) { joinBody: '─', joinLeft: '├', joinRight: '┤', - joinJoin: '┼' + joinJoin: '┼', }; - return table( - data, - { - border: Object - .keys( border ) - .reduce( ( accumulator, key ) => ( { - ...accumulator, - [ key ]: chalk.grey( border[ key ] ), - } ), {} ), - }, - ); + return table(data, { + border: Object.keys(border).reduce( + (accumulator, key) => ({ + ...accumulator, + [key]: chalk.grey(border[key]), + }), + {}, + ), + }); }; diff --git a/src/utils/yaml.js b/src/utils/yaml.js index 9f3e0393..5ead18e4 100644 --- a/src/utils/yaml.js +++ b/src/utils/yaml.js @@ -1,18 +1,18 @@ -const readYaml = require( 'read-yaml' ); -const writeYaml = require( 'write-yaml' ); +const readYaml = require('read-yaml'); +const writeYaml = require('write-yaml'); -exports.readYaml = function( filename ) { - return readYaml.sync( filename, {} ); +exports.readYaml = function (filename) { + return readYaml.sync(filename, {}); }; -exports.writeYaml = function( filename, data ) { - return new Promise( ( resolve, reject ) => { - writeYaml( filename, data, { 'lineWidth': 500 }, ( err ) => { - if ( err ) { - reject( err ); +exports.writeYaml = function (filename, data) { + return new Promise((resolve, reject) => { + writeYaml(filename, data, { lineWidth: 500 }, (err) => { + if (err) { + reject(err); } else { resolve(); } - } ); - } ); + }); + }); }; diff --git a/test/integration/certificates.test.js b/test/integration/certificates.test.js index 737b76a9..0686b1dd 100644 --- a/test/integration/certificates.test.js +++ b/test/integration/certificates.test.js @@ -2,239 +2,222 @@ let mockExecSync; -jest.mock( 'child_process', () => ( { - execSync: ( ...args ) => mockExecSync( ...args ), -} ) ); +jest.mock('child_process', () => ({ + execSync: (...args) => mockExecSync(...args), +})); const mockReadFile = jest.fn(); const mockWriteFile = jest.fn(); -jest.mock( 'fs', () => ( { +jest.mock('fs', () => ({ promises: { - readFile: ( ...args ) => mockReadFile( ...args ), - writeFile: ( ...args ) => mockWriteFile( ...args ), + readFile: (...args) => mockReadFile(...args), + writeFile: (...args) => mockWriteFile(...args), }, -} ) ); +})); const mockCreateCert = jest.fn(); -jest.mock( 'mkcert', () => ( { - createCert: ( ...args ) => mockCreateCert( ...args ), -} ) ); +jest.mock('mkcert', () => ({ + createCert: (...args) => mockCreateCert(...args), +})); -jest.mock( 'mkcert-prebuilt', () => '/usr/local/bin/mkcert' ); +jest.mock('mkcert-prebuilt', () => '/usr/local/bin/mkcert'); const mockEnvSlug = jest.fn(); -jest.mock( '../../src/env-utils', () => ( { - envSlug: ( ...args ) => mockEnvSlug( ...args ), -} ) ); +jest.mock('../../src/env-utils', () => ({ + envSlug: (...args) => mockEnvSlug(...args), +})); const mockGetSslCertsDirectory = jest.fn(); -jest.mock( '../../src/configure', () => ( { - getSslCertsDirectory: ( ...args ) => mockGetSslCertsDirectory( ...args ), -} ) ); +jest.mock('../../src/configure', () => ({ + getSslCertsDirectory: (...args) => mockGetSslCertsDirectory(...args), +})); -const { getCARoot, installCA, generate } = require( '../../src/certificates' ); +const { getCARoot, installCA, generate } = require('../../src/certificates'); -beforeEach( () => { +beforeEach(() => { mockExecSync = jest.fn(); -} ); +}); -describe( 'getCARoot', () => { - it( 'calls mkcert-prebuilt with -CAROOT and encoding utf-8', () => { - mockExecSync.mockReturnValue( '/home/user/.local/share/mkcert\n' ); +describe('getCARoot', () => { + it('calls mkcert-prebuilt with -CAROOT and encoding utf-8', () => { + mockExecSync.mockReturnValue('/home/user/.local/share/mkcert\n'); getCARoot(); - expect( mockExecSync ).toHaveBeenCalledWith( - '"/usr/local/bin/mkcert" -CAROOT', - { encoding: 'utf-8' } - ); - } ); + expect(mockExecSync).toHaveBeenCalledWith('"/usr/local/bin/mkcert" -CAROOT', { + encoding: 'utf-8', + }); + }); - it( 'returns the trimmed CAROOT path', () => { - mockExecSync.mockReturnValue( ' /home/user/.local/share/mkcert\n ' ); + it('returns the trimmed CAROOT path', () => { + mockExecSync.mockReturnValue(' /home/user/.local/share/mkcert\n '); const result = getCARoot(); - expect( result ).toBe( '/home/user/.local/share/mkcert' ); - } ); -} ); + expect(result).toBe('/home/user/.local/share/mkcert'); + }); +}); -describe( 'installCA', () => { - it( 'calls mkcert-prebuilt with -install', () => { - mockExecSync.mockReturnValue( undefined ); +describe('installCA', () => { + it('calls mkcert-prebuilt with -install', () => { + mockExecSync.mockReturnValue(undefined); installCA(); - expect( mockExecSync ).toHaveBeenCalledWith( + expect(mockExecSync).toHaveBeenCalledWith( '"/usr/local/bin/mkcert" -install', - expect.objectContaining( {} ) + expect.objectContaining({}), ); - } ); + }); - it( 'uses stdio ignore by default', () => { - mockExecSync.mockReturnValue( undefined ); + it('uses stdio ignore by default', () => { + mockExecSync.mockReturnValue(undefined); installCA(); - expect( mockExecSync ).toHaveBeenCalledWith( - expect.any( String ), - { stdio: 'ignore' } - ); - } ); + expect(mockExecSync).toHaveBeenCalledWith(expect.any(String), { stdio: 'ignore' }); + }); - it( 'uses stdio inherit when verbose=true', () => { - mockExecSync.mockReturnValue( undefined ); + it('uses stdio inherit when verbose=true', () => { + mockExecSync.mockReturnValue(undefined); - installCA( true ); + installCA(true); - expect( mockExecSync ).toHaveBeenCalledWith( - expect.any( String ), - { stdio: 'inherit' } - ); - } ); + expect(mockExecSync).toHaveBeenCalledWith(expect.any(String), { stdio: 'inherit' }); + }); - it( 'returns true on success', () => { - mockExecSync.mockReturnValue( undefined ); + it('returns true on success', () => { + mockExecSync.mockReturnValue(undefined); const result = installCA(); - expect( result ).toBe( true ); - } ); + expect(result).toBe(true); + }); - it( 'returns false when execSync throws', () => { - mockExecSync.mockImplementation( () => { - throw new Error( 'mkcert not found' ); - } ); + it('returns false when execSync throws', () => { + mockExecSync.mockImplementation(() => { + throw new Error('mkcert not found'); + }); const result = installCA(); - expect( result ).toBe( false ); - } ); + expect(result).toBe(false); + }); - it( 'returns false without logging when verbose=false and execSync throws', () => { - mockExecSync.mockImplementation( () => { - throw new Error( 'mkcert not found' ); - } ); - const consoleSpy = jest.spyOn( console, 'error' ).mockImplementation( () => {} ); + it('returns false without logging when verbose=false and execSync throws', () => { + mockExecSync.mockImplementation(() => { + throw new Error('mkcert not found'); + }); + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - installCA( false ); + installCA(false); - expect( consoleSpy ).not.toHaveBeenCalled(); + expect(consoleSpy).not.toHaveBeenCalled(); consoleSpy.mockRestore(); - } ); + }); - it( 'logs the error when verbose=true and execSync throws', () => { - const err = new Error( 'mkcert not found' ); - mockExecSync.mockImplementation( () => { + it('logs the error when verbose=true and execSync throws', () => { + const err = new Error('mkcert not found'); + mockExecSync.mockImplementation(() => { throw err; - } ); - const consoleSpy = jest.spyOn( console, 'error' ).mockImplementation( () => {} ); + }); + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - installCA( true ); + installCA(true); - expect( consoleSpy ).toHaveBeenCalledWith( err ); + expect(consoleSpy).toHaveBeenCalledWith(err); consoleSpy.mockRestore(); - } ); -} ); + }); +}); -describe( 'generate', () => { +describe('generate', () => { const caRoot = '/home/user/.local/share/mkcert'; - beforeEach( () => { - mockExecSync.mockReturnValue( `${ caRoot }\n` ); - mockEnvSlug.mockReturnValue( 'my-env' ); - mockGetSslCertsDirectory.mockResolvedValue( '/etc/ssl/certs/wp' ); - mockReadFile.mockImplementation( ( filePath ) => { - if ( filePath.endsWith( 'rootCA-key.pem' ) ) { - return Promise.resolve( '---KEY---' ); + beforeEach(() => { + mockExecSync.mockReturnValue(`${caRoot}\n`); + mockEnvSlug.mockReturnValue('my-env'); + mockGetSslCertsDirectory.mockResolvedValue('/etc/ssl/certs/wp'); + mockReadFile.mockImplementation((filePath) => { + if (filePath.endsWith('rootCA-key.pem')) { + return Promise.resolve('---KEY---'); } - return Promise.resolve( '---CERT---' ); - } ); - mockCreateCert.mockResolvedValue( { cert: 'CERT_CONTENT', key: 'KEY_CONTENT' } ); - mockWriteFile.mockResolvedValue( undefined ); - } ); + return Promise.resolve('---CERT---'); + }); + mockCreateCert.mockResolvedValue({ cert: 'CERT_CONTENT', key: 'KEY_CONTENT' }); + mockWriteFile.mockResolvedValue(undefined); + }); - it( 'slugifies the envName via envSlug', async () => { - await generate( 'My Env', [ 'example.com' ] ); + it('slugifies the envName via envSlug', async () => { + await generate('My Env', ['example.com']); - expect( mockEnvSlug ).toHaveBeenCalledWith( 'My Env' ); - } ); + expect(mockEnvSlug).toHaveBeenCalledWith('My Env'); + }); - it( 'reads rootCA-key.pem from the CAROOT directory', async () => { - await generate( 'myenv', [ 'example.com' ] ); + it('reads rootCA-key.pem from the CAROOT directory', async () => { + await generate('myenv', ['example.com']); - expect( mockReadFile ).toHaveBeenCalledWith( - `${ caRoot }/rootCA-key.pem`, - { encoding: 'utf-8' } - ); - } ); + expect(mockReadFile).toHaveBeenCalledWith(`${caRoot}/rootCA-key.pem`, { + encoding: 'utf-8', + }); + }); - it( 'reads rootCA.pem from the CAROOT directory', async () => { - await generate( 'myenv', [ 'example.com' ] ); + it('reads rootCA.pem from the CAROOT directory', async () => { + await generate('myenv', ['example.com']); - expect( mockReadFile ).toHaveBeenCalledWith( - `${ caRoot }/rootCA.pem`, - { encoding: 'utf-8' } - ); - } ); + expect(mockReadFile).toHaveBeenCalledWith(`${caRoot}/rootCA.pem`, { encoding: 'utf-8' }); + }); - it( 'calls mkcert.createCert with hosts and their wildcards', async () => { - await generate( 'myenv', [ 'example.com', 'test.local' ] ); + it('calls mkcert.createCert with hosts and their wildcards', async () => { + await generate('myenv', ['example.com', 'test.local']); - expect( mockCreateCert ).toHaveBeenCalledWith( { + expect(mockCreateCert).toHaveBeenCalledWith({ caCert: '---CERT---', caKey: '---KEY---', - domains: [ 'example.com', 'test.local', '*.example.com', '*.test.local' ], + domains: ['example.com', 'test.local', '*.example.com', '*.test.local'], validityDays: 365, - } ); - } ); + }); + }); - it( 'calls mkcert.createCert with caCert and caKey from the read files', async () => { - mockReadFile.mockImplementation( ( filePath ) => { - if ( filePath.endsWith( 'rootCA-key.pem' ) ) { - return Promise.resolve( 'MY_CA_KEY' ); + it('calls mkcert.createCert with caCert and caKey from the read files', async () => { + mockReadFile.mockImplementation((filePath) => { + if (filePath.endsWith('rootCA-key.pem')) { + return Promise.resolve('MY_CA_KEY'); } - return Promise.resolve( 'MY_CA_CERT' ); - } ); + return Promise.resolve('MY_CA_CERT'); + }); - await generate( 'myenv', [ 'example.com' ] ); + await generate('myenv', ['example.com']); - expect( mockCreateCert ).toHaveBeenCalledWith( - expect.objectContaining( { + expect(mockCreateCert).toHaveBeenCalledWith( + expect.objectContaining({ caCert: 'MY_CA_CERT', caKey: 'MY_CA_KEY', - } ) + }), ); - } ); + }); - it( 'writes the cert file to sslDir/.crt', async () => { - await generate( 'myenv', [ 'example.com' ] ); + it('writes the cert file to sslDir/.crt', async () => { + await generate('myenv', ['example.com']); - expect( mockWriteFile ).toHaveBeenCalledWith( - '/etc/ssl/certs/wp/my-env.crt', - 'CERT_CONTENT' - ); - } ); + expect(mockWriteFile).toHaveBeenCalledWith('/etc/ssl/certs/wp/my-env.crt', 'CERT_CONTENT'); + }); - it( 'writes the key file to sslDir/.key', async () => { - await generate( 'myenv', [ 'example.com' ] ); + it('writes the key file to sslDir/.key', async () => { + await generate('myenv', ['example.com']); - expect( mockWriteFile ).toHaveBeenCalledWith( - '/etc/ssl/certs/wp/my-env.key', - 'KEY_CONTENT' - ); - } ); + expect(mockWriteFile).toHaveBeenCalledWith('/etc/ssl/certs/wp/my-env.key', 'KEY_CONTENT'); + }); - it( 'returns the cert and key file paths', async () => { - const result = await generate( 'myenv', [ 'example.com' ] ); + it('returns the cert and key file paths', async () => { + const result = await generate('myenv', ['example.com']); - expect( result ).toEqual( { + expect(result).toEqual({ cert: '/etc/ssl/certs/wp/my-env.crt', key: '/etc/ssl/certs/wp/my-env.key', - } ); - } ); -} ); + }); + }); +}); diff --git a/test/integration/create/make-docker-compose.test.js b/test/integration/create/make-docker-compose.test.js index 8d312ffd..1cf027b8 100644 --- a/test/integration/create/make-docker-compose.test.js +++ b/test/integration/create/make-docker-compose.test.js @@ -1,38 +1,38 @@ 'use strict'; -jest.mock( '../../../src/configure' ); +jest.mock('../../../src/configure'); // Variable must be prefixed with 'mock' to be accessible inside jest.mock factory const mockOs = { platform: jest.fn(), }; -jest.mock( 'os', () => mockOs ); +jest.mock('os', () => mockOs); -const { images } = require( '../../../src/docker-images' ); -const { cacheVolume } = require( '../../../src/env-utils' ); +const { images } = require('../../../src/docker-images'); +const { cacheVolume } = require('../../../src/env-utils'); const SNAPSHOTS_PATH = '/home/testuser/.wpsnapshots'; -describe( 'makeDockerCompose', () => { +describe('makeDockerCompose', () => { let makeDockerCompose; let mockConfig; - beforeEach( () => { + beforeEach(() => { jest.resetModules(); // Re-require after resetModules so mocks are fresh - jest.mock( '../../../src/configure' ); - mockConfig = require( '../../../src/configure' ); - mockConfig.get = jest.fn().mockResolvedValue( SNAPSHOTS_PATH ); + jest.mock('../../../src/configure'); + mockConfig = require('../../../src/configure'); + mockConfig.get = jest.fn().mockResolvedValue(SNAPSHOTS_PATH); // Default to non-linux platform - mockOs.platform.mockReturnValue( 'darwin' ); + mockOs.platform.mockReturnValue('darwin'); // After mocks are set up, require the module under test - makeDockerCompose = require( '../../../src/commands/create/make-docker-compose' ); - } ); + makeDockerCompose = require('../../../src/commands/create/make-docker-compose'); + }); - function baseSettings( overrides = {} ) { + function baseSettings(overrides = {}) { return { envSlug: 'mysite-test', php: '8.2', @@ -43,179 +43,188 @@ describe( 'makeDockerCompose', () => { }; } - const baseHosts = [ 'mysite.test' ]; + const baseHosts = ['mysite.test']; // ----------------------------------------------------------------------- // PHP version → correct image // ----------------------------------------------------------------------- - it( 'uses the correct phpfpm image for php8.2', async () => { - const fn = makeDockerCompose( null ); - const result = await fn( baseHosts, baseSettings( { php: '8.2' } ) ); - expect( result.services.phpfpm.image ).toBe( images['php8.2'] ); - } ); + it('uses the correct phpfpm image for php8.2', async () => { + const fn = makeDockerCompose(null); + const result = await fn(baseHosts, baseSettings({ php: '8.2' })); + expect(result.services.phpfpm.image).toBe(images['php8.2']); + }); - it( 'uses the correct phpfpm image for php7.4', async () => { - const fn = makeDockerCompose( null ); - const result = await fn( baseHosts, baseSettings( { php: '7.4' } ) ); - expect( result.services.phpfpm.image ).toBe( images['php7.4'] ); - } ); + it('uses the correct phpfpm image for php7.4', async () => { + const fn = makeDockerCompose(null); + const result = await fn(baseHosts, baseSettings({ php: '7.4' })); + expect(result.services.phpfpm.image).toBe(images['php7.4']); + }); // ----------------------------------------------------------------------- // CERT_NAME: certs on/off // ----------------------------------------------------------------------- - it( 'sets CERT_NAME to envSlug when certs is true', async () => { - const fn = makeDockerCompose( null ); - const result = await fn( baseHosts, baseSettings( { certs: true, envSlug: 'mysite-test' } ) ); - expect( result.services.nginx.environment.CERT_NAME ).toBe( 'mysite-test' ); - } ); + it('sets CERT_NAME to envSlug when certs is true', async () => { + const fn = makeDockerCompose(null); + const result = await fn(baseHosts, baseSettings({ certs: true, envSlug: 'mysite-test' })); + expect(result.services.nginx.environment.CERT_NAME).toBe('mysite-test'); + }); - it( 'sets CERT_NAME to localhost when certs is false', async () => { - const fn = makeDockerCompose( null ); - const result = await fn( baseHosts, baseSettings( { certs: false } ) ); - expect( result.services.nginx.environment.CERT_NAME ).toBe( 'localhost' ); - } ); + it('sets CERT_NAME to localhost when certs is false', async () => { + const fn = makeDockerCompose(null); + const result = await fn(baseHosts, baseSettings({ certs: false })); + expect(result.services.nginx.environment.CERT_NAME).toBe('localhost'); + }); // ----------------------------------------------------------------------- // VIRTUAL_HOST includes wildcard hosts // ----------------------------------------------------------------------- - it( 'sets VIRTUAL_HOST to host + wildcard', async () => { - const fn = makeDockerCompose( null ); - const result = await fn( [ 'mysite.test' ], baseSettings() ); - expect( result.services.nginx.environment.VIRTUAL_HOST ).toBe( 'mysite.test,*.mysite.test' ); - } ); + it('sets VIRTUAL_HOST to host + wildcard', async () => { + const fn = makeDockerCompose(null); + const result = await fn(['mysite.test'], baseSettings()); + expect(result.services.nginx.environment.VIRTUAL_HOST).toBe('mysite.test,*.mysite.test'); + }); // ----------------------------------------------------------------------- // wordpress type: dev branch vs non-dev // ----------------------------------------------------------------------- - it( 'uses develop.conf nginx config and wp-cli develop volume when type is dev', async () => { - const fn = makeDockerCompose( null ); - const result = await fn( baseHosts, baseSettings( { wordpress: { type: 'dev' } } ) ); + it('uses develop.conf nginx config and wp-cli develop volume when type is dev', async () => { + const fn = makeDockerCompose(null); + const result = await fn(baseHosts, baseSettings({ wordpress: { type: 'dev' } })); const nginxVolumes = result.services.nginx.volumes; const phpfpmVolumes = result.services.phpfpm.volumes; - expect( nginxVolumes.some( ( v ) => v.includes( 'develop.conf' ) ) ).toBe( true ); - expect( phpfpmVolumes.some( ( v ) => v.includes( 'wp-cli.develop.yml' ) ) ).toBe( true ); - } ); + expect(nginxVolumes.some((v) => v.includes('develop.conf'))).toBe(true); + expect(phpfpmVolumes.some((v) => v.includes('wp-cli.develop.yml'))).toBe(true); + }); - it( 'uses default.conf nginx config and wp-cli local volume when type is not dev', async () => { - const fn = makeDockerCompose( null ); - const result = await fn( baseHosts, baseSettings( { wordpress: { type: 'standard' } } ) ); + it('uses default.conf nginx config and wp-cli local volume when type is not dev', async () => { + const fn = makeDockerCompose(null); + const result = await fn(baseHosts, baseSettings({ wordpress: { type: 'standard' } })); const nginxVolumes = result.services.nginx.volumes; const phpfpmVolumes = result.services.phpfpm.volumes; - expect( nginxVolumes.some( ( v ) => v.includes( 'default.conf' ) ) ).toBe( true ); - expect( phpfpmVolumes.some( ( v ) => v.includes( 'wp-cli.local.yml' ) ) ).toBe( true ); - } ); + expect(nginxVolumes.some((v) => v.includes('default.conf'))).toBe(true); + expect(phpfpmVolumes.some((v) => v.includes('wp-cli.local.yml'))).toBe(true); + }); // ----------------------------------------------------------------------- // Elasticsearch: on/off // ----------------------------------------------------------------------- - it( 'does not add elasticsearch service when elasticsearch is false', async () => { - const fn = makeDockerCompose( null ); - const result = await fn( baseHosts, baseSettings( { elasticsearch: false } ) ); - expect( result.services.elasticsearch ).toBeUndefined(); - expect( result.volumes.elasticsearchData ).toBeUndefined(); - } ); + it('does not add elasticsearch service when elasticsearch is false', async () => { + const fn = makeDockerCompose(null); + const result = await fn(baseHosts, baseSettings({ elasticsearch: false })); + expect(result.services.elasticsearch).toBeUndefined(); + expect(result.volumes.elasticsearchData).toBeUndefined(); + }); - it( 'adds elasticsearch service and volume when elasticsearch is true', async () => { - const fn = makeDockerCompose( null ); - const result = await fn( baseHosts, baseSettings( { elasticsearch: true } ) ); - expect( result.services.elasticsearch ).toBeDefined(); - expect( result.services.elasticsearch.image ).toBe( images['elasticsearch'] ); - expect( result.services.elasticsearch.expose ).toEqual( [ '9200' ] ); - expect( result.volumes.elasticsearchData ).toBeDefined(); - expect( result.services.phpfpm.depends_on ).toContain( 'elasticsearch' ); - } ); + it('adds elasticsearch service and volume when elasticsearch is true', async () => { + const fn = makeDockerCompose(null); + const result = await fn(baseHosts, baseSettings({ elasticsearch: true })); + expect(result.services.elasticsearch).toBeDefined(); + expect(result.services.elasticsearch.image).toBe(images['elasticsearch']); + expect(result.services.elasticsearch.expose).toEqual(['9200']); + expect(result.volumes.elasticsearchData).toBeDefined(); + expect(result.services.phpfpm.depends_on).toContain('elasticsearch'); + }); // ----------------------------------------------------------------------- // Non-Linux branch: wpsnapshots volume uses www-data // ----------------------------------------------------------------------- - it( 'mounts wpsnapshots under /home/www-data on non-linux', async () => { - mockOs.platform.mockReturnValue( 'darwin' ); - const fn = makeDockerCompose( null ); - const result = await fn( baseHosts, baseSettings() ); + it('mounts wpsnapshots under /home/www-data on non-linux', async () => { + mockOs.platform.mockReturnValue('darwin'); + const fn = makeDockerCompose(null); + const result = await fn(baseHosts, baseSettings()); const vols = result.services.phpfpm.volumes; - expect( vols.some( ( v ) => v.includes( `${ SNAPSHOTS_PATH }:/home/www-data/.wpsnapshots` ) ) ).toBe( true ); - } ); + expect(vols.some((v) => v.includes(`${SNAPSHOTS_PATH}:/home/www-data/.wpsnapshots`))).toBe( + true, + ); + }); // ----------------------------------------------------------------------- // Linux branch: custom image, build args, user-scoped volumes // ----------------------------------------------------------------------- - it( 'uses custom image and build args on linux', async () => { - mockOs.platform.mockReturnValue( 'linux' ); + it('uses custom image and build args on linux', async () => { + mockOs.platform.mockReturnValue('linux'); const savedUser = process.env.USER; const savedGetuid = process.getuid; process.env.USER = 'devuser'; process.getuid = () => 1001; - const fn = makeDockerCompose( null ); - const result = await fn( baseHosts, baseSettings( { php: '8.2' } ) ); + const fn = makeDockerCompose(null); + const result = await fn(baseHosts, baseSettings({ php: '8.2' })); - expect( result.services.phpfpm.image ).toBe( 'wp-php-fpm-dev-8.2-devuser' ); - expect( result.services.phpfpm.build.args.CALLING_USER ).toBe( 'devuser' ); - expect( result.services.phpfpm.build.args.CALLING_UID ).toBe( 1001 ); - expect( result.services.phpfpm.build.args.PHP_IMAGE ).toBe( images['php8.2'] ); - expect( result.services.phpfpm.volumes.some( ( v ) => v.includes( '/home/devuser/.wpsnapshots' ) ) ).toBe( true ); + expect(result.services.phpfpm.image).toBe('wp-php-fpm-dev-8.2-devuser'); + expect(result.services.phpfpm.build.args.CALLING_USER).toBe('devuser'); + expect(result.services.phpfpm.build.args.CALLING_UID).toBe(1001); + expect(result.services.phpfpm.build.args.PHP_IMAGE).toBe(images['php8.2']); + expect( + result.services.phpfpm.volumes.some((v) => v.includes('/home/devuser/.wpsnapshots')), + ).toBe(true); process.env.USER = savedUser; process.getuid = savedGetuid; - } ); + }); // ----------------------------------------------------------------------- // cacheVolume in phpfpm volumes and named volumes section // ----------------------------------------------------------------------- - it( 'includes cacheVolume in phpfpm volumes and top-level volumes', async () => { - const fn = makeDockerCompose( null ); - const result = await fn( baseHosts, baseSettings() ); - expect( result.services.phpfpm.volumes.some( ( v ) => v.startsWith( `${ cacheVolume }:` ) ) ).toBe( true ); - expect( result.volumes[ cacheVolume ] ).toBeDefined(); - expect( result.volumes[ cacheVolume ].name ).toBe( cacheVolume ); - } ); + it('includes cacheVolume in phpfpm volumes and top-level volumes', async () => { + const fn = makeDockerCompose(null); + const result = await fn(baseHosts, baseSettings()); + expect(result.services.phpfpm.volumes.some((v) => v.startsWith(`${cacheVolume}:`))).toBe( + true, + ); + expect(result.volumes[cacheVolume]).toBeDefined(); + expect(result.volumes[cacheVolume].name).toBe(cacheVolume); + }); // ----------------------------------------------------------------------- // dockerCompose filter hook // ----------------------------------------------------------------------- - it( 'applies dockerCompose filter when it is a function and returns a value', async () => { + it('applies dockerCompose filter when it is a function and returns a value', async () => { const customConfig = { custom: true }; - const settings = baseSettings( { - dockerCompose: jest.fn().mockResolvedValue( customConfig ), - } ); - const fn = makeDockerCompose( null ); - const result = await fn( baseHosts, settings ); - expect( result ).toEqual( { custom: true } ); - expect( settings.dockerCompose ).toHaveBeenCalled(); - } ); - - it( 'keeps original config when dockerCompose filter returns falsy', async () => { - const settings = baseSettings( { - dockerCompose: jest.fn().mockResolvedValue( null ), - } ); - const fn = makeDockerCompose( null ); - const result = await fn( baseHosts, settings ); - expect( result.services.nginx ).toBeDefined(); - } ); + const settings = baseSettings({ + dockerCompose: jest.fn().mockResolvedValue(customConfig), + }); + const fn = makeDockerCompose(null); + const result = await fn(baseHosts, settings); + expect(result).toEqual({ custom: true }); + expect(settings.dockerCompose).toHaveBeenCalled(); + }); + + it('keeps original config when dockerCompose filter returns falsy', async () => { + const settings = baseSettings({ + dockerCompose: jest.fn().mockResolvedValue(null), + }); + const fn = makeDockerCompose(null); + const result = await fn(baseHosts, settings); + expect(result.services.nginx).toBeDefined(); + }); // ----------------------------------------------------------------------- // Spinner path: called with spinner calls start/succeed // ----------------------------------------------------------------------- - it( 'calls spinner.start and spinner.succeed when spinner is provided', async () => { + it('calls spinner.start and spinner.succeed when spinner is provided', async () => { const spinner = { start: jest.fn(), succeed: jest.fn() }; - const fn = makeDockerCompose( spinner ); - await fn( baseHosts, baseSettings() ); - expect( spinner.start ).toHaveBeenCalledTimes( 1 ); - expect( spinner.succeed ).toHaveBeenCalledTimes( 1 ); - } ); + const fn = makeDockerCompose(spinner); + await fn(baseHosts, baseSettings()); + expect(spinner.start).toHaveBeenCalledTimes(1); + expect(spinner.succeed).toHaveBeenCalledTimes(1); + }); - it( 'does not throw when spinner is null', async () => { - const fn = makeDockerCompose( null ); - await expect( fn( baseHosts, baseSettings() ) ).resolves.toBeDefined(); - } ); + it('does not throw when spinner is null', async () => { + const fn = makeDockerCompose(null); + await expect(fn(baseHosts, baseSettings())).resolves.toBeDefined(); + }); // ----------------------------------------------------------------------- // Snapshot: full non-linux standard config // ----------------------------------------------------------------------- - it( 'matches snapshot for a standard non-linux environment', async () => { - mockOs.platform.mockReturnValue( 'darwin' ); - const fn = makeDockerCompose( null ); - const result = await fn( [ 'mysite.test' ], baseSettings( { certs: true, envSlug: 'mysite-test', php: '8.2' } ) ); - expect( result ).toMatchSnapshot(); - } ); -} ); + it('matches snapshot for a standard non-linux environment', async () => { + mockOs.platform.mockReturnValue('darwin'); + const fn = makeDockerCompose(null); + const result = await fn( + ['mysite.test'], + baseSettings({ certs: true, envSlug: 'mysite-test', php: '8.2' }), + ); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/test/integration/database.test.js b/test/integration/database.test.js index 09bb1cfe..656b6621 100644 --- a/test/integration/database.test.js +++ b/test/integration/database.test.js @@ -4,124 +4,124 @@ let mockQuery; let mockDestroy; let mockConnection; -jest.mock( 'mysql', () => ( { - createConnection: jest.fn( () => mockConnection ), -} ) ); +jest.mock('mysql', () => ({ + createConnection: jest.fn(() => mockConnection), +})); -const mysql = require( 'mysql' ); -const { create, deleteDatabase, assignPrivs } = require( '../../src/database' ); +const mysql = require('mysql'); +const { create, deleteDatabase, assignPrivs } = require('../../src/database'); -beforeEach( () => { +beforeEach(() => { mockDestroy = jest.fn(); mockQuery = jest.fn(); mockConnection = { query: mockQuery, destroy: mockDestroy }; - mysql.createConnection.mockReturnValue( mockConnection ); -} ); + mysql.createConnection.mockReturnValue(mockConnection); +}); -describe( 'create', () => { - it( 'issues CREATE DATABASE IF NOT EXISTS with correct dbname', async () => { - mockQuery.mockImplementation( ( sql, cb ) => cb( null ) ); +describe('create', () => { + it('issues CREATE DATABASE IF NOT EXISTS with correct dbname', async () => { + mockQuery.mockImplementation((sql, cb) => cb(null)); - await create( 'mydb' ); + await create('mydb'); - expect( mockQuery ).toHaveBeenCalledWith( + expect(mockQuery).toHaveBeenCalledWith( 'CREATE DATABASE IF NOT EXISTS `mydb`;', - expect.any( Function ) + expect.any(Function), ); - } ); + }); - it( 'calls destroy on success', async () => { - mockQuery.mockImplementation( ( sql, cb ) => cb( null ) ); + it('calls destroy on success', async () => { + mockQuery.mockImplementation((sql, cb) => cb(null)); - await create( 'mydb' ); + await create('mydb'); - expect( mockDestroy ).toHaveBeenCalledTimes( 1 ); - } ); + expect(mockDestroy).toHaveBeenCalledTimes(1); + }); - it( 'calls destroy before rejecting on error', async () => { + it('calls destroy before rejecting on error', async () => { const calls = []; - mockDestroy.mockImplementation( () => calls.push( 'destroy' ) ); - mockQuery.mockImplementation( ( sql, cb ) => cb( new Error( 'query failed' ) ) ); + mockDestroy.mockImplementation(() => calls.push('destroy')); + mockQuery.mockImplementation((sql, cb) => cb(new Error('query failed'))); - await expect( create( 'mydb' ) ).rejects.toThrow(); - expect( calls ).toContain( 'destroy' ); - } ); + await expect(create('mydb')).rejects.toThrow(); + expect(calls).toContain('destroy'); + }); - it( 'rejects with an Error when query errors', async () => { - mockQuery.mockImplementation( ( sql, cb ) => cb( new Error( 'query failed' ) ) ); + it('rejects with an Error when query errors', async () => { + mockQuery.mockImplementation((sql, cb) => cb(new Error('query failed'))); - await expect( create( 'mydb' ) ).rejects.toBeInstanceOf( Error ); - } ); -} ); + await expect(create('mydb')).rejects.toBeInstanceOf(Error); + }); +}); -describe( 'deleteDatabase', () => { - it( 'issues DROP DATABASE IF EXISTS with correct dbname', async () => { - mockQuery.mockImplementation( ( sql, cb ) => cb( null ) ); +describe('deleteDatabase', () => { + it('issues DROP DATABASE IF EXISTS with correct dbname', async () => { + mockQuery.mockImplementation((sql, cb) => cb(null)); - await deleteDatabase( 'mydb' ); + await deleteDatabase('mydb'); - expect( mockQuery ).toHaveBeenCalledWith( + expect(mockQuery).toHaveBeenCalledWith( 'DROP DATABASE IF EXISTS `mydb`;', - expect.any( Function ) + expect.any(Function), ); - } ); + }); - it( 'calls destroy on success', async () => { - mockQuery.mockImplementation( ( sql, cb ) => cb( null ) ); + it('calls destroy on success', async () => { + mockQuery.mockImplementation((sql, cb) => cb(null)); - await deleteDatabase( 'mydb' ); + await deleteDatabase('mydb'); - expect( mockDestroy ).toHaveBeenCalledTimes( 1 ); - } ); + expect(mockDestroy).toHaveBeenCalledTimes(1); + }); - it( 'calls destroy before rejecting on error', async () => { + it('calls destroy before rejecting on error', async () => { const calls = []; - mockDestroy.mockImplementation( () => calls.push( 'destroy' ) ); - mockQuery.mockImplementation( ( sql, cb ) => cb( new Error( 'drop failed' ) ) ); + mockDestroy.mockImplementation(() => calls.push('destroy')); + mockQuery.mockImplementation((sql, cb) => cb(new Error('drop failed'))); - await expect( deleteDatabase( 'mydb' ) ).rejects.toThrow(); - expect( calls ).toContain( 'destroy' ); - } ); + await expect(deleteDatabase('mydb')).rejects.toThrow(); + expect(calls).toContain('destroy'); + }); - it( 'rejects with an Error when query errors', async () => { - mockQuery.mockImplementation( ( sql, cb ) => cb( new Error( 'drop failed' ) ) ); + it('rejects with an Error when query errors', async () => { + mockQuery.mockImplementation((sql, cb) => cb(new Error('drop failed'))); - await expect( deleteDatabase( 'mydb' ) ).rejects.toBeInstanceOf( Error ); - } ); -} ); + await expect(deleteDatabase('mydb')).rejects.toBeInstanceOf(Error); + }); +}); -describe( 'assignPrivs', () => { - it( 'issues GRANT ALL PRIVILEGES with correct dbname', async () => { - mockQuery.mockImplementation( ( sql, cb ) => cb( null ) ); +describe('assignPrivs', () => { + it('issues GRANT ALL PRIVILEGES with correct dbname', async () => { + mockQuery.mockImplementation((sql, cb) => cb(null)); - await assignPrivs( 'mydb' ); + await assignPrivs('mydb'); - expect( mockQuery ).toHaveBeenCalledWith( - 'GRANT ALL PRIVILEGES ON `mydb`.* TO \'wordpress\'@\'%\' IDENTIFIED BY \'password\';', - expect.any( Function ) + expect(mockQuery).toHaveBeenCalledWith( + "GRANT ALL PRIVILEGES ON `mydb`.* TO 'wordpress'@'%' IDENTIFIED BY 'password';", + expect.any(Function), ); - } ); + }); - it( 'calls destroy on success', async () => { - mockQuery.mockImplementation( ( sql, cb ) => cb( null ) ); + it('calls destroy on success', async () => { + mockQuery.mockImplementation((sql, cb) => cb(null)); - await assignPrivs( 'mydb' ); + await assignPrivs('mydb'); - expect( mockDestroy ).toHaveBeenCalledTimes( 1 ); - } ); + expect(mockDestroy).toHaveBeenCalledTimes(1); + }); - it( 'calls destroy before rejecting on error', async () => { + it('calls destroy before rejecting on error', async () => { const calls = []; - mockDestroy.mockImplementation( () => calls.push( 'destroy' ) ); - mockQuery.mockImplementation( ( sql, cb ) => cb( new Error( 'grant failed' ) ) ); + mockDestroy.mockImplementation(() => calls.push('destroy')); + mockQuery.mockImplementation((sql, cb) => cb(new Error('grant failed'))); - await expect( assignPrivs( 'mydb' ) ).rejects.toThrow(); - expect( calls ).toContain( 'destroy' ); - } ); + await expect(assignPrivs('mydb')).rejects.toThrow(); + expect(calls).toContain('destroy'); + }); - it( 'rejects with an Error when query errors', async () => { - mockQuery.mockImplementation( ( sql, cb ) => cb( new Error( 'grant failed' ) ) ); + it('rejects with an Error when query errors', async () => { + mockQuery.mockImplementation((sql, cb) => cb(new Error('grant failed'))); - await expect( assignPrivs( 'mydb' ) ).rejects.toBeInstanceOf( Error ); - } ); -} ); + await expect(assignPrivs('mydb')).rejects.toBeInstanceOf(Error); + }); +}); diff --git a/test/integration/environment.test.js b/test/integration/environment.test.js index 3a5c6187..18a1371b 100644 --- a/test/integration/environment.test.js +++ b/test/integration/environment.test.js @@ -40,17 +40,17 @@ const mockInquirer = { const mockWhich = jest.fn(); const mockSudo = { exec: jest.fn() }; -jest.mock( '../../src/configure', () => mockConfig ); -jest.mock( '../../src/database', () => mockDatabase ); -jest.mock( '../../src/env-utils', () => mockEnvUtils ); -jest.mock( '../../src/gateway', () => mockGateway ); -jest.mock( '../../src/utils/docker-compose', () => mockCompose ); -jest.mock( 'fs-extra', () => mockFsExtra ); -jest.mock( 'inquirer', () => mockInquirer ); -jest.mock( 'which', () => mockWhich ); -jest.mock( '@vscode/sudo-prompt', () => mockSudo ); - -const environment = require( '../../src/environment' ); +jest.mock('../../src/configure', () => mockConfig); +jest.mock('../../src/database', () => mockDatabase); +jest.mock('../../src/env-utils', () => mockEnvUtils); +jest.mock('../../src/gateway', () => mockGateway); +jest.mock('../../src/utils/docker-compose', () => mockCompose); +jest.mock('fs-extra', () => mockFsExtra); +jest.mock('inquirer', () => mockInquirer); +jest.mock('which', () => mockWhich); +jest.mock('@vscode/sudo-prompt', () => mockSudo); + +const environment = require('../../src/environment'); function makeSpinner() { return { @@ -63,224 +63,230 @@ function makeSpinner() { const ENV_NAME = 'mysite-test'; const ENV_PATH = '/sites/mysite-test'; const ENV_SLUG = 'mysite-test'; -const ENV_HOSTS = [ 'mysite.test', 'www.mysite.test' ]; +const ENV_HOSTS = ['mysite.test', 'www.mysite.test']; const SSL_DIR = '/home/user/.wplocaldocker/global/ssl-certs'; -beforeEach( () => { - mockEnvUtils.getPathOrError.mockResolvedValue( ENV_PATH ); - mockEnvUtils.envSlug.mockReturnValue( ENV_SLUG ); - mockEnvUtils.getEnvHosts.mockResolvedValue( ENV_HOSTS ); - mockEnvUtils.getAllEnvironments.mockResolvedValue( [ ENV_NAME ] ); +beforeEach(() => { + mockEnvUtils.getPathOrError.mockResolvedValue(ENV_PATH); + mockEnvUtils.envSlug.mockReturnValue(ENV_SLUG); + mockEnvUtils.getEnvHosts.mockResolvedValue(ENV_HOSTS); + mockEnvUtils.getAllEnvironments.mockResolvedValue([ENV_NAME]); mockGateway.startGlobal.mockResolvedValue(); mockGateway.stopGlobal.mockResolvedValue(); mockGateway.restartGlobal.mockResolvedValue(); mockCompose.upAll.mockResolvedValue(); mockCompose.down.mockResolvedValue(); mockCompose.pullAll.mockResolvedValue(); - mockCompose.isRunning.mockResolvedValue( false ); + mockCompose.isRunning.mockResolvedValue(false); mockDatabase.deleteDatabase.mockResolvedValue(); mockFsExtra.remove.mockResolvedValue(); - mockConfig.getSslCertsDirectory.mockResolvedValue( SSL_DIR ); - mockConfig.get.mockResolvedValue( false ); - mockInquirer.prompt.mockResolvedValue( { confirm: true } ); - mockWhich.mockResolvedValue( '/usr/local/bin/node' ); - mockSudo.exec.mockImplementation( ( cmd, opts, cb ) => cb( null, 'ok' ) ); -} ); - -describe( 'environment', () => { - describe( 'start', () => { - it( 'calls startGlobal and upAll without pulling when pull=false', async () => { + mockConfig.getSslCertsDirectory.mockResolvedValue(SSL_DIR); + mockConfig.get.mockResolvedValue(false); + mockInquirer.prompt.mockResolvedValue({ confirm: true }); + mockWhich.mockResolvedValue('/usr/local/bin/node'); + mockSudo.exec.mockImplementation((cmd, opts, cb) => cb(null, 'ok')); +}); + +describe('environment', () => { + describe('start', () => { + it('calls startGlobal and upAll without pulling when pull=false', async () => { const spinner = makeSpinner(); - await environment.start( ENV_NAME, spinner, false ); + await environment.start(ENV_NAME, spinner, false); - expect( mockEnvUtils.getPathOrError ).toHaveBeenCalledWith( ENV_NAME, spinner ); - expect( mockGateway.startGlobal ).toHaveBeenCalledWith( spinner, false ); - expect( mockCompose.pullAll ).not.toHaveBeenCalled(); - expect( mockCompose.upAll ).toHaveBeenCalledWith( { cwd: ENV_PATH, log: false } ); - } ); + expect(mockEnvUtils.getPathOrError).toHaveBeenCalledWith(ENV_NAME, spinner); + expect(mockGateway.startGlobal).toHaveBeenCalledWith(spinner, false); + expect(mockCompose.pullAll).not.toHaveBeenCalled(); + expect(mockCompose.upAll).toHaveBeenCalledWith({ cwd: ENV_PATH, log: false }); + }); - it( 'pulls images before upAll when pull=true', async () => { + it('pulls images before upAll when pull=true', async () => { const spinner = makeSpinner(); - await environment.start( ENV_NAME, spinner, true ); + await environment.start(ENV_NAME, spinner, true); - expect( mockCompose.pullAll ).toHaveBeenCalledWith( { cwd: ENV_PATH, log: false } ); - expect( mockCompose.upAll ).toHaveBeenCalledWith( { cwd: ENV_PATH, log: false } ); - } ); + expect(mockCompose.pullAll).toHaveBeenCalledWith({ cwd: ENV_PATH, log: false }); + expect(mockCompose.upAll).toHaveBeenCalledWith({ cwd: ENV_PATH, log: false }); + }); - it( 'sets log=true when no spinner is provided', async () => { - await environment.start( ENV_NAME, null, false ); + it('sets log=true when no spinner is provided', async () => { + await environment.start(ENV_NAME, null, false); - expect( mockCompose.upAll ).toHaveBeenCalledWith( { cwd: ENV_PATH, log: true } ); - } ); + expect(mockCompose.upAll).toHaveBeenCalledWith({ cwd: ENV_PATH, log: true }); + }); - it( 'calls spinner.start and spinner.succeed', async () => { + it('calls spinner.start and spinner.succeed', async () => { const spinner = makeSpinner(); - await environment.start( ENV_NAME, spinner, false ); + await environment.start(ENV_NAME, spinner, false); - expect( spinner.start ).toHaveBeenCalled(); - expect( spinner.succeed ).toHaveBeenCalled(); - } ); - } ); + expect(spinner.start).toHaveBeenCalled(); + expect(spinner.succeed).toHaveBeenCalled(); + }); + }); - describe( 'stop', () => { - it( 'calls compose.down with the env cwd', async () => { + describe('stop', () => { + it('calls compose.down with the env cwd', async () => { const spinner = makeSpinner(); - await environment.stop( ENV_NAME, spinner ); + await environment.stop(ENV_NAME, spinner); - expect( mockEnvUtils.getPathOrError ).toHaveBeenCalledWith( ENV_NAME, spinner ); - expect( mockCompose.down ).toHaveBeenCalledWith( { cwd: ENV_PATH, log: false } ); - } ); + expect(mockEnvUtils.getPathOrError).toHaveBeenCalledWith(ENV_NAME, spinner); + expect(mockCompose.down).toHaveBeenCalledWith({ cwd: ENV_PATH, log: false }); + }); - it( 'sets log=true when no spinner is provided', async () => { - await environment.stop( ENV_NAME, null ); + it('sets log=true when no spinner is provided', async () => { + await environment.stop(ENV_NAME, null); - expect( mockCompose.down ).toHaveBeenCalledWith( { cwd: ENV_PATH, log: true } ); - } ); - } ); + expect(mockCompose.down).toHaveBeenCalledWith({ cwd: ENV_PATH, log: true }); + }); + }); - describe( 'restart', () => { - it( 'calls compose.down before upAll when isRunning=true', async () => { - mockCompose.isRunning.mockResolvedValue( true ); + describe('restart', () => { + it('calls compose.down before upAll when isRunning=true', async () => { + mockCompose.isRunning.mockResolvedValue(true); const callOrder = []; - mockCompose.down.mockImplementation( () => { callOrder.push( 'down' ); return Promise.resolve(); } ); - mockCompose.upAll.mockImplementation( () => { callOrder.push( 'upAll' ); return Promise.resolve(); } ); + mockCompose.down.mockImplementation(() => { + callOrder.push('down'); + return Promise.resolve(); + }); + mockCompose.upAll.mockImplementation(() => { + callOrder.push('upAll'); + return Promise.resolve(); + }); const spinner = makeSpinner(); - await environment.restart( ENV_NAME, spinner ); + await environment.restart(ENV_NAME, spinner); - expect( callOrder ).toEqual( [ 'down', 'upAll' ] ); - } ); + expect(callOrder).toEqual(['down', 'upAll']); + }); - it( 'skips compose.down when isRunning=false', async () => { - mockCompose.isRunning.mockResolvedValue( false ); + it('skips compose.down when isRunning=false', async () => { + mockCompose.isRunning.mockResolvedValue(false); const spinner = makeSpinner(); - await environment.restart( ENV_NAME, spinner ); + await environment.restart(ENV_NAME, spinner); - expect( mockCompose.down ).not.toHaveBeenCalled(); - expect( mockCompose.upAll ).toHaveBeenCalled(); - } ); + expect(mockCompose.down).not.toHaveBeenCalled(); + expect(mockCompose.upAll).toHaveBeenCalled(); + }); - it( 'calls startGlobal and then upAll', async () => { + it('calls startGlobal and then upAll', async () => { const spinner = makeSpinner(); - await environment.restart( ENV_NAME, spinner ); + await environment.restart(ENV_NAME, spinner); - expect( mockGateway.startGlobal ).toHaveBeenCalledWith( spinner ); - expect( mockCompose.upAll ).toHaveBeenCalledWith( { cwd: ENV_PATH, log: false } ); - } ); - } ); + expect(mockGateway.startGlobal).toHaveBeenCalledWith(spinner); + expect(mockCompose.upAll).toHaveBeenCalledWith({ cwd: ENV_PATH, log: false }); + }); + }); - describe( 'deleteEnv', () => { - it( 'returns early without removing anything when confirm=false', async () => { - mockInquirer.prompt.mockResolvedValue( { confirm: false } ); + describe('deleteEnv', () => { + it('returns early without removing anything when confirm=false', async () => { + mockInquirer.prompt.mockResolvedValue({ confirm: false }); const spinner = makeSpinner(); - await environment.deleteEnv( ENV_NAME, spinner ); + await environment.deleteEnv(ENV_NAME, spinner); - expect( mockFsExtra.remove ).not.toHaveBeenCalled(); - expect( mockDatabase.deleteDatabase ).not.toHaveBeenCalled(); - } ); + expect(mockFsExtra.remove).not.toHaveBeenCalled(); + expect(mockDatabase.deleteDatabase).not.toHaveBeenCalled(); + }); - it( 'removes env directory, certificates, and database when confirm=true', async () => { - mockInquirer.prompt.mockResolvedValue( { confirm: true } ); + it('removes env directory, certificates, and database when confirm=true', async () => { + mockInquirer.prompt.mockResolvedValue({ confirm: true }); const spinner = makeSpinner(); - await environment.deleteEnv( ENV_NAME, spinner ); + await environment.deleteEnv(ENV_NAME, spinner); - expect( mockFsExtra.remove ).toHaveBeenCalledWith( ENV_PATH ); - expect( mockFsExtra.remove ).toHaveBeenCalledWith( `${ SSL_DIR }/${ ENV_SLUG }.crt` ); - expect( mockFsExtra.remove ).toHaveBeenCalledWith( `${ SSL_DIR }/${ ENV_SLUG }.key` ); - expect( mockDatabase.deleteDatabase ).toHaveBeenCalledWith( ENV_SLUG ); - } ); + expect(mockFsExtra.remove).toHaveBeenCalledWith(ENV_PATH); + expect(mockFsExtra.remove).toHaveBeenCalledWith(`${SSL_DIR}/${ENV_SLUG}.crt`); + expect(mockFsExtra.remove).toHaveBeenCalledWith(`${SSL_DIR}/${ENV_SLUG}.key`); + expect(mockDatabase.deleteDatabase).toHaveBeenCalledWith(ENV_SLUG); + }); - it( 'calls compose.down with -v commandOptions', async () => { - mockInquirer.prompt.mockResolvedValue( { confirm: true } ); - await environment.deleteEnv( ENV_NAME, null ); + it('calls compose.down with -v commandOptions', async () => { + mockInquirer.prompt.mockResolvedValue({ confirm: true }); + await environment.deleteEnv(ENV_NAME, null); - expect( mockCompose.down ).toHaveBeenCalledWith( { + expect(mockCompose.down).toHaveBeenCalledWith({ cwd: ENV_PATH, log: true, - commandOptions: [ '-v' ], - } ); - } ); + commandOptions: ['-v'], + }); + }); - it( 'calls getSslCertsDirectory with false to skip dir creation', async () => { - mockInquirer.prompt.mockResolvedValue( { confirm: true } ); - await environment.deleteEnv( ENV_NAME, null ); + it('calls getSslCertsDirectory with false to skip dir creation', async () => { + mockInquirer.prompt.mockResolvedValue({ confirm: true }); + await environment.deleteEnv(ENV_NAME, null); - expect( mockConfig.getSslCertsDirectory ).toHaveBeenCalledWith( false ); - } ); + expect(mockConfig.getSslCertsDirectory).toHaveBeenCalledWith(false); + }); - it( 'calls sudo.exec to remove host entries when manageHosts=true', async () => { - mockConfig.get.mockResolvedValue( true ); - mockInquirer.prompt.mockResolvedValue( { confirm: true } ); + it('calls sudo.exec to remove host entries when manageHosts=true', async () => { + mockConfig.get.mockResolvedValue(true); + mockInquirer.prompt.mockResolvedValue({ confirm: true }); const spinner = makeSpinner(); - await environment.deleteEnv( ENV_NAME, spinner ); + await environment.deleteEnv(ENV_NAME, spinner); - expect( mockWhich ).toHaveBeenCalledWith( 'node' ); - expect( mockSudo.exec ).toHaveBeenCalled(); - } ); + expect(mockWhich).toHaveBeenCalledWith('node'); + expect(mockSudo.exec).toHaveBeenCalled(); + }); - it( 'does not call sudo.exec when manageHosts=false', async () => { - mockConfig.get.mockResolvedValue( false ); - mockInquirer.prompt.mockResolvedValue( { confirm: true } ); - await environment.deleteEnv( ENV_NAME, null ); + it('does not call sudo.exec when manageHosts=false', async () => { + mockConfig.get.mockResolvedValue(false); + mockInquirer.prompt.mockResolvedValue({ confirm: true }); + await environment.deleteEnv(ENV_NAME, null); - expect( mockSudo.exec ).not.toHaveBeenCalled(); - } ); + expect(mockSudo.exec).not.toHaveBeenCalled(); + }); - it( 'calls spinner.warn when sudo.exec callback receives an error', async () => { - mockConfig.get.mockResolvedValue( true ); - mockInquirer.prompt.mockResolvedValue( { confirm: true } ); - mockSudo.exec.mockImplementation( ( cmd, opts, cb ) => cb( new Error( 'sudo failed' ) ) ); + it('calls spinner.warn when sudo.exec callback receives an error', async () => { + mockConfig.get.mockResolvedValue(true); + mockInquirer.prompt.mockResolvedValue({ confirm: true }); + mockSudo.exec.mockImplementation((cmd, opts, cb) => cb(new Error('sudo failed'))); const spinner = makeSpinner(); - await environment.deleteEnv( ENV_NAME, spinner ); + await environment.deleteEnv(ENV_NAME, spinner); - expect( spinner.warn ).toHaveBeenCalled(); - } ); - } ); + expect(spinner.warn).toHaveBeenCalled(); + }); + }); - describe( 'startAll', () => { - it( 'calls startGlobal once up-front and start for each environment', async () => { - mockEnvUtils.getAllEnvironments.mockResolvedValue( [ 'site-a', 'site-b' ] ); + describe('startAll', () => { + it('calls startGlobal once up-front and start for each environment', async () => { + mockEnvUtils.getAllEnvironments.mockResolvedValue(['site-a', 'site-b']); const spinner = makeSpinner(); - await environment.startAll( spinner, false ); + await environment.startAll(spinner, false); - expect( mockEnvUtils.getAllEnvironments ).toHaveBeenCalled(); + expect(mockEnvUtils.getAllEnvironments).toHaveBeenCalled(); // startGlobal: 1 from startAll + 1 per start() call = 3 total - expect( mockGateway.startGlobal ).toHaveBeenCalledTimes( 3 ); - expect( mockCompose.upAll ).toHaveBeenCalledTimes( 2 ); - } ); - } ); - - describe( 'stopAll', () => { - it( 'stops each environment then calls stopGlobal', async () => { - mockEnvUtils.getAllEnvironments.mockResolvedValue( [ 'site-a', 'site-b' ] ); + expect(mockGateway.startGlobal).toHaveBeenCalledTimes(3); + expect(mockCompose.upAll).toHaveBeenCalledTimes(2); + }); + }); + + describe('stopAll', () => { + it('stops each environment then calls stopGlobal', async () => { + mockEnvUtils.getAllEnvironments.mockResolvedValue(['site-a', 'site-b']); const spinner = makeSpinner(); - await environment.stopAll( spinner ); + await environment.stopAll(spinner); - expect( mockCompose.down ).toHaveBeenCalledTimes( 2 ); - expect( mockGateway.stopGlobal ).toHaveBeenCalledWith( spinner ); - } ); - } ); + expect(mockCompose.down).toHaveBeenCalledTimes(2); + expect(mockGateway.stopGlobal).toHaveBeenCalledWith(spinner); + }); + }); - describe( 'restartAll', () => { - it( 'restarts each environment then calls restartGlobal', async () => { - mockEnvUtils.getAllEnvironments.mockResolvedValue( [ 'site-a', 'site-b' ] ); + describe('restartAll', () => { + it('restarts each environment then calls restartGlobal', async () => { + mockEnvUtils.getAllEnvironments.mockResolvedValue(['site-a', 'site-b']); const spinner = makeSpinner(); - await environment.restartAll( spinner ); + await environment.restartAll(spinner); - expect( mockCompose.upAll ).toHaveBeenCalledTimes( 2 ); - expect( mockGateway.restartGlobal ).toHaveBeenCalledWith( spinner ); - } ); - } ); + expect(mockCompose.upAll).toHaveBeenCalledTimes(2); + expect(mockGateway.restartGlobal).toHaveBeenCalledWith(spinner); + }); + }); - describe( 'deleteAll', () => { - it( 'calls deleteEnv for each environment', async () => { - mockEnvUtils.getAllEnvironments.mockResolvedValue( [ 'site-a', 'site-b' ] ); - mockInquirer.prompt.mockResolvedValue( { confirm: false } ); + describe('deleteAll', () => { + it('calls deleteEnv for each environment', async () => { + mockEnvUtils.getAllEnvironments.mockResolvedValue(['site-a', 'site-b']); + mockInquirer.prompt.mockResolvedValue({ confirm: false }); const spinner = makeSpinner(); - await environment.deleteAll( spinner ); + await environment.deleteAll(spinner); - expect( mockEnvUtils.getAllEnvironments ).toHaveBeenCalled(); - expect( mockInquirer.prompt ).toHaveBeenCalledTimes( 2 ); - } ); - } ); -} ); + expect(mockEnvUtils.getAllEnvironments).toHaveBeenCalled(); + expect(mockInquirer.prompt).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/test/integration/gateway.test.js b/test/integration/gateway.test.js index 29bc1eb1..2d7b9a9e 100644 --- a/test/integration/gateway.test.js +++ b/test/integration/gateway.test.js @@ -1,6 +1,6 @@ 'use strict'; -const EventEmitter = require( 'events' ); +const EventEmitter = require('events'); // Tracks nc instances created; tests can assert on them or emit 'data' to resolve waitForDB let mockNcInstances = []; @@ -11,11 +11,11 @@ function mockMakeNc() { const inst = new EventEmitter(); inst.address = jest.fn(); inst.port = jest.fn(); - inst.connect = jest.fn().mockImplementation( () => { - setImmediate( () => inst.emit( 'data' ) ); - } ); + inst.connect = jest.fn().mockImplementation(() => { + setImmediate(() => inst.emit('data')); + }); inst.close = jest.fn(); - mockNcInstances.push( inst ); + mockNcInstances.push(inst); return inst; } @@ -35,13 +35,13 @@ const mockEnvUtils = { cacheVolume: 'wplocaldockerCache', }; -jest.mock( '../../src/utils/docker-compose', () => mockCompose ); -jest.mock( '../../src/configure', () => mockConfig ); -jest.mock( '../../src/env-utils', () => mockEnvUtils ); -jest.mock( 'fs', () => ( { existsSync: jest.fn( () => false ) } ) ); -jest.mock( 'netcat/client', () => jest.fn( () => mockMakeNc() ) ); +jest.mock('../../src/utils/docker-compose', () => mockCompose); +jest.mock('../../src/configure', () => mockConfig); +jest.mock('../../src/env-utils', () => mockEnvUtils); +jest.mock('fs', () => ({ existsSync: jest.fn(() => false) })); +jest.mock('netcat/client', () => jest.fn(() => mockMakeNc())); // make-docker is mocked via a var so we can swap mockDocker per test -jest.mock( '../../src/utils/make-docker', () => () => mockDocker ); // eslint-disable-line no-undef +jest.mock('../../src/utils/make-docker', () => () => mockDocker); // eslint-disable-line no-undef function makeDockerMock() { const network = { @@ -55,8 +55,8 @@ function makeDockerMock() { return { _network: network, _volume: volume, - getNetwork: jest.fn( () => network ), - getVolume: jest.fn( () => volume ), + getNetwork: jest.fn(() => network), + getVolume: jest.fn(() => volume), createNetwork: jest.fn(), createVolume: jest.fn(), }; @@ -74,198 +74,204 @@ function makeSpinner() { }; } -beforeEach( () => { +beforeEach(() => { mockDocker = makeDockerMock(); mockNcInstances = []; mockCompose.upAll.mockResolvedValue(); mockCompose.down.mockResolvedValue(); mockCompose.pullAll.mockResolvedValue(); mockCompose.restartAll.mockResolvedValue(); - mockConfig.getConfigDirectory.mockReturnValue( '/home/user/.wplocaldocker' ); -} ); + mockConfig.getConfigDirectory.mockReturnValue('/home/user/.wplocaldocker'); +}); // Gateway has module-level `started` flag; reload per test to reset it. function loadGateway() { jest.resetModules(); - jest.mock( '../../src/utils/make-docker', () => () => mockDocker ); - jest.mock( '../../src/utils/docker-compose', () => mockCompose ); - jest.mock( '../../src/configure', () => mockConfig ); - jest.mock( '../../src/env-utils', () => mockEnvUtils ); - jest.mock( 'fs', () => ( { existsSync: jest.fn( () => false ) } ) ); - jest.mock( 'netcat/client', () => jest.fn( () => mockMakeNc() ) ); - return require( '../../src/gateway' ); + jest.mock('../../src/utils/make-docker', () => () => mockDocker); + jest.mock('../../src/utils/docker-compose', () => mockCompose); + jest.mock('../../src/configure', () => mockConfig); + jest.mock('../../src/env-utils', () => mockEnvUtils); + jest.mock('fs', () => ({ existsSync: jest.fn(() => false) })); + jest.mock('netcat/client', () => jest.fn(() => mockMakeNc())); + return require('../../src/gateway'); } -describe( 'gateway', () => { - describe( 'ensureNetworkExists', () => { - it( 'does nothing when the network already exists', async () => { +describe('gateway', () => { + describe('ensureNetworkExists', () => { + it('does nothing when the network already exists', async () => { const gateway = loadGateway(); - mockDocker._network.inspect.mockResolvedValue( { Id: 'abc' } ); + mockDocker._network.inspect.mockResolvedValue({ Id: 'abc' }); - await gateway.ensureNetworkExists( mockDocker, makeSpinner() ); + await gateway.ensureNetworkExists(mockDocker, makeSpinner()); - expect( mockDocker.createNetwork ).not.toHaveBeenCalled(); - } ); + expect(mockDocker.createNetwork).not.toHaveBeenCalled(); + }); - it( 'creates the network when inspect rejects', async () => { + it('creates the network when inspect rejects', async () => { const gateway = loadGateway(); - mockDocker._network.inspect.mockRejectedValue( new Error( 'not found' ) ); + mockDocker._network.inspect.mockRejectedValue(new Error('not found')); mockDocker.createNetwork.mockResolvedValue(); - await gateway.ensureNetworkExists( mockDocker, makeSpinner() ); + await gateway.ensureNetworkExists(mockDocker, makeSpinner()); - expect( mockDocker.createNetwork ).toHaveBeenCalledWith( - expect.objectContaining( { Name: 'wplocaldocker' } ) + expect(mockDocker.createNetwork).toHaveBeenCalledWith( + expect.objectContaining({ Name: 'wplocaldocker' }), ); - } ); + }); - it( 'logs to console when no spinner is provided', async () => { + it('logs to console when no spinner is provided', async () => { const gateway = loadGateway(); - mockDocker._network.inspect.mockResolvedValue( { Id: 'abc' } ); - const spy = jest.spyOn( console, 'log' ).mockImplementation( () => {} ); + mockDocker._network.inspect.mockResolvedValue({ Id: 'abc' }); + const spy = jest.spyOn(console, 'log').mockImplementation(() => {}); - await gateway.ensureNetworkExists( mockDocker, null ); + await gateway.ensureNetworkExists(mockDocker, null); - expect( spy ).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); spy.mockRestore(); - } ); - } ); + }); + }); - describe( 'ensureCacheExists', () => { - it( 'does nothing when the volume already exists', async () => { + describe('ensureCacheExists', () => { + it('does nothing when the volume already exists', async () => { const gateway = loadGateway(); - mockDocker._volume.inspect.mockResolvedValue( { Name: mockEnvUtils.cacheVolume } ); + mockDocker._volume.inspect.mockResolvedValue({ Name: mockEnvUtils.cacheVolume }); - await gateway.ensureCacheExists( mockDocker, makeSpinner() ); + await gateway.ensureCacheExists(mockDocker, makeSpinner()); - expect( mockDocker.createVolume ).not.toHaveBeenCalled(); - } ); + expect(mockDocker.createVolume).not.toHaveBeenCalled(); + }); - it( 'creates the volume when inspect rejects', async () => { + it('creates the volume when inspect rejects', async () => { const gateway = loadGateway(); - mockDocker._volume.inspect.mockRejectedValue( new Error( 'not found' ) ); + mockDocker._volume.inspect.mockRejectedValue(new Error('not found')); mockDocker.createVolume.mockResolvedValue(); - await gateway.ensureCacheExists( mockDocker, makeSpinner() ); + await gateway.ensureCacheExists(mockDocker, makeSpinner()); - expect( mockDocker.createVolume ).toHaveBeenCalledWith( - expect.objectContaining( { Name: mockEnvUtils.cacheVolume } ) + expect(mockDocker.createVolume).toHaveBeenCalledWith( + expect.objectContaining({ Name: mockEnvUtils.cacheVolume }), ); - } ); - } ); + }); + }); - describe( 'removeCacheVolume', () => { - it( 'removes the volume when it exists', async () => { + describe('removeCacheVolume', () => { + it('removes the volume when it exists', async () => { const gateway = loadGateway(); - mockDocker._volume.inspect.mockResolvedValue( { Name: mockEnvUtils.cacheVolume } ); + mockDocker._volume.inspect.mockResolvedValue({ Name: mockEnvUtils.cacheVolume }); mockDocker._volume.remove.mockResolvedValue(); - await gateway.removeCacheVolume( mockDocker, makeSpinner() ); + await gateway.removeCacheVolume(mockDocker, makeSpinner()); - expect( mockDocker._volume.remove ).toHaveBeenCalled(); - } ); + expect(mockDocker._volume.remove).toHaveBeenCalled(); + }); - it( 'skips removal when the volume does not exist', async () => { + it('skips removal when the volume does not exist', async () => { const gateway = loadGateway(); - mockDocker._volume.inspect.mockRejectedValue( new Error( 'not found' ) ); + mockDocker._volume.inspect.mockRejectedValue(new Error('not found')); - await gateway.removeCacheVolume( mockDocker, makeSpinner() ); + await gateway.removeCacheVolume(mockDocker, makeSpinner()); - expect( mockDocker._volume.remove ).not.toHaveBeenCalled(); - } ); - } ); + expect(mockDocker._volume.remove).not.toHaveBeenCalled(); + }); + }); - describe( 'startGlobal', () => { - beforeEach( () => { + describe('startGlobal', () => { + beforeEach(() => { jest.useFakeTimers(); - } ); + }); - afterEach( () => { + afterEach(() => { jest.useRealTimers(); - } ); + }); - it( 'calls getNetwork, getVolume and compose.upAll', async () => { + it('calls getNetwork, getVolume and compose.upAll', async () => { const gateway = loadGateway(); - mockDocker._network.inspect.mockResolvedValue( {} ); - mockDocker._volume.inspect.mockResolvedValue( {} ); + mockDocker._network.inspect.mockResolvedValue({}); + mockDocker._volume.inspect.mockResolvedValue({}); - const p = gateway.startGlobal( makeSpinner(), false ); + const p = gateway.startGlobal(makeSpinner(), false); await jest.runAllTimersAsync(); await p; - expect( mockDocker.getNetwork ).toHaveBeenCalledWith( 'wplocaldocker' ); - expect( mockDocker.getVolume ).toHaveBeenCalledWith( mockEnvUtils.cacheVolume ); - expect( mockCompose.upAll ).toHaveBeenCalled(); - } ); + expect(mockDocker.getNetwork).toHaveBeenCalledWith('wplocaldocker'); + expect(mockDocker.getVolume).toHaveBeenCalledWith(mockEnvUtils.cacheVolume); + expect(mockCompose.upAll).toHaveBeenCalled(); + }); - it( 'is idempotent — second call is a no-op', async () => { + it('is idempotent — second call is a no-op', async () => { const gateway = loadGateway(); - mockDocker._network.inspect.mockResolvedValue( {} ); - mockDocker._volume.inspect.mockResolvedValue( {} ); + mockDocker._network.inspect.mockResolvedValue({}); + mockDocker._volume.inspect.mockResolvedValue({}); - const p1 = gateway.startGlobal( makeSpinner(), false ); + const p1 = gateway.startGlobal(makeSpinner(), false); await jest.runAllTimersAsync(); await p1; - await gateway.startGlobal( makeSpinner(), false ); + await gateway.startGlobal(makeSpinner(), false); - expect( mockCompose.upAll ).toHaveBeenCalledTimes( 1 ); - } ); + expect(mockCompose.upAll).toHaveBeenCalledTimes(1); + }); - it( 'calls pullAll before upAll when pull=true', async () => { + it('calls pullAll before upAll when pull=true', async () => { const gateway = loadGateway(); - mockDocker._network.inspect.mockResolvedValue( {} ); - mockDocker._volume.inspect.mockResolvedValue( {} ); + mockDocker._network.inspect.mockResolvedValue({}); + mockDocker._volume.inspect.mockResolvedValue({}); const callOrder = []; - mockCompose.pullAll.mockImplementation( () => { callOrder.push( 'pullAll' ); return Promise.resolve(); } ); - mockCompose.upAll.mockImplementation( () => { callOrder.push( 'upAll' ); return Promise.resolve(); } ); - - const p = gateway.startGlobal( makeSpinner(), true ); + mockCompose.pullAll.mockImplementation(() => { + callOrder.push('pullAll'); + return Promise.resolve(); + }); + mockCompose.upAll.mockImplementation(() => { + callOrder.push('upAll'); + return Promise.resolve(); + }); + + const p = gateway.startGlobal(makeSpinner(), true); await jest.runAllTimersAsync(); await p; - expect( callOrder ).toEqual( [ 'pullAll', 'upAll' ] ); - } ); - } ); + expect(callOrder).toEqual(['pullAll', 'upAll']); + }); + }); - describe( 'stopGlobal', () => { - it( 'calls compose.down and removes the network', async () => { + describe('stopGlobal', () => { + it('calls compose.down and removes the network', async () => { const gateway = loadGateway(); - mockDocker._network.inspect.mockResolvedValue( {} ); + mockDocker._network.inspect.mockResolvedValue({}); mockDocker._network.remove.mockResolvedValue(); - await gateway.stopGlobal( makeSpinner() ); + await gateway.stopGlobal(makeSpinner()); - expect( mockCompose.down ).toHaveBeenCalled(); - expect( mockDocker._network.remove ).toHaveBeenCalled(); - } ); + expect(mockCompose.down).toHaveBeenCalled(); + expect(mockDocker._network.remove).toHaveBeenCalled(); + }); - it( 'swallows errors and resolves cleanly', async () => { + it('swallows errors and resolves cleanly', async () => { const gateway = loadGateway(); - mockCompose.down.mockRejectedValue( new Error( 'compose error' ) ); + mockCompose.down.mockRejectedValue(new Error('compose error')); - await expect( gateway.stopGlobal( makeSpinner() ) ).resolves.toBeUndefined(); - } ); - } ); + await expect(gateway.stopGlobal(makeSpinner())).resolves.toBeUndefined(); + }); + }); - describe( 'restartGlobal', () => { - it( 'calls ensureNetworkExists then compose.restartAll', async () => { + describe('restartGlobal', () => { + it('calls ensureNetworkExists then compose.restartAll', async () => { const gateway = loadGateway(); - mockDocker._network.inspect.mockResolvedValue( {} ); + mockDocker._network.inspect.mockResolvedValue({}); - await gateway.restartGlobal( makeSpinner() ); + await gateway.restartGlobal(makeSpinner()); - expect( mockDocker.getNetwork ).toHaveBeenCalledWith( 'wplocaldocker' ); - expect( mockCompose.restartAll ).toHaveBeenCalled(); - } ); + expect(mockDocker.getNetwork).toHaveBeenCalledWith('wplocaldocker'); + expect(mockCompose.restartAll).toHaveBeenCalled(); + }); - it( 'swallows errors and resolves cleanly', async () => { + it('swallows errors and resolves cleanly', async () => { const gateway = loadGateway(); - mockDocker._network.inspect.mockResolvedValue( {} ); - mockCompose.restartAll.mockRejectedValue( new Error( 'restart error' ) ); + mockDocker._network.inspect.mockResolvedValue({}); + mockCompose.restartAll.mockRejectedValue(new Error('restart error')); - await expect( gateway.restartGlobal( makeSpinner() ) ).resolves.toBeUndefined(); - } ); - } ); -} ); + await expect(gateway.restartGlobal(makeSpinner())).resolves.toBeUndefined(); + }); + }); +}); diff --git a/test/integration/utils/docker-compose.test.js b/test/integration/utils/docker-compose.test.js index 94d68fd5..d5afaeeb 100644 --- a/test/integration/utils/docker-compose.test.js +++ b/test/integration/utils/docker-compose.test.js @@ -12,60 +12,60 @@ const mockCompose = { port: jest.fn(), }; -jest.mock( 'docker-compose', () => mockCompose ); +jest.mock('docker-compose', () => mockCompose); -const dc = require( '../../../src/utils/docker-compose' ); +const dc = require('../../../src/utils/docker-compose'); -function resolvesWith( out ) { - return Promise.resolve( { out, err: '', exitCode: 0 } ); +function resolvesWith(out) { + return Promise.resolve({ out, err: '', exitCode: 0 }); } -function rejectsWith( err ) { - return Promise.resolve( { out: '', err, exitCode: 1 } ); +function rejectsWith(err) { + return Promise.resolve({ out: '', err, exitCode: 1 }); } -const PROXIED_METHODS = [ 'down', 'exec', 'logs', 'ps', 'pullAll', 'restartAll', 'run', 'upAll' ]; +const PROXIED_METHODS = ['down', 'exec', 'logs', 'ps', 'pullAll', 'restartAll', 'run', 'upAll']; -describe( 'docker-compose wrapper', () => { - describe( 'proxied methods forward args and return out', () => { - PROXIED_METHODS.forEach( ( method ) => { - it( `${ method } forwards args and resolves with out`, async () => { - mockCompose[ method ].mockReturnValue( resolvesWith( `${ method } output` ) ); +describe('docker-compose wrapper', () => { + describe('proxied methods forward args and return out', () => { + PROXIED_METHODS.forEach((method) => { + it(`${method} forwards args and resolves with out`, async () => { + mockCompose[method].mockReturnValue(resolvesWith(`${method} output`)); - const result = await dc[ method ]( 'arg1', 'arg2' ); + const result = await dc[method]('arg1', 'arg2'); - expect( mockCompose[ method ] ).toHaveBeenCalledWith( 'arg1', 'arg2' ); - expect( result ).toBe( `${ method } output` ); - } ); - } ); - } ); + expect(mockCompose[method]).toHaveBeenCalledWith('arg1', 'arg2'); + expect(result).toBe(`${method} output`); + }); + }); + }); - describe( 'proxied methods throw when exitCode is truthy', () => { - PROXIED_METHODS.forEach( ( method ) => { - it( `${ method } throws the err string on non-zero exitCode`, async () => { - mockCompose[ method ].mockReturnValue( rejectsWith( `${ method } failed` ) ); + describe('proxied methods throw when exitCode is truthy', () => { + PROXIED_METHODS.forEach((method) => { + it(`${method} throws the err string on non-zero exitCode`, async () => { + mockCompose[method].mockReturnValue(rejectsWith(`${method} failed`)); - await expect( dc[ method ]() ).rejects.toThrow( `${ method } failed` ); - } ); - } ); - } ); + await expect(dc[method]()).rejects.toThrow(`${method} failed`); + }); + }); + }); - describe( 'isRunning', () => { - it( 'returns true when compose.port resolves', async () => { - mockCompose.port.mockResolvedValue( {} ); + describe('isRunning', () => { + it('returns true when compose.port resolves', async () => { + mockCompose.port.mockResolvedValue({}); - const result = await dc.isRunning( '/some/cwd' ); + const result = await dc.isRunning('/some/cwd'); - expect( result ).toBe( true ); - expect( mockCompose.port ).toHaveBeenCalledWith( 'nginx', 80, { cwd: '/some/cwd' } ); - } ); + expect(result).toBe(true); + expect(mockCompose.port).toHaveBeenCalledWith('nginx', 80, { cwd: '/some/cwd' }); + }); - it( 'returns false when compose.port rejects', async () => { - mockCompose.port.mockRejectedValue( new Error( 'not running' ) ); + it('returns false when compose.port rejects', async () => { + mockCompose.port.mockRejectedValue(new Error('not running')); - const result = await dc.isRunning( '/some/cwd' ); + const result = await dc.isRunning('/some/cwd'); - expect( result ).toBe( false ); - } ); - } ); -} ); + expect(result).toBe(false); + }); + }); +}); diff --git a/test/unit/configure.test.js b/test/unit/configure.test.js index 6aba0e8c..f7080b94 100644 --- a/test/unit/configure.test.js +++ b/test/unit/configure.test.js @@ -1,81 +1,81 @@ 'use strict'; -const path = require( 'path' ); +const path = require('path'); -jest.mock( 'os', () => ( { - ...jest.requireActual( 'os' ), +jest.mock('os', () => ({ + ...jest.requireActual('os'), homedir: () => '/fake/home', -} ) ); +})); const { createProxyConfig, getDefaults, getConfigDirectory, getGlobalDirectory, -} = require( '../../src/configure' ); +} = require('../../src/configure'); -describe( 'getConfigDirectory', () => { - it( 'returns .wplocaldocker inside the home directory', () => { - expect( getConfigDirectory() ).toBe( path.join( '/fake/home', '.wplocaldocker' ) ); - } ); -} ); +describe('getConfigDirectory', () => { + it('returns .wplocaldocker inside the home directory', () => { + expect(getConfigDirectory()).toBe(path.join('/fake/home', '.wplocaldocker')); + }); +}); -describe( 'getGlobalDirectory', () => { - it( 'returns global/ inside the config directory', () => { - expect( getGlobalDirectory() ).toBe( path.join( '/fake/home', '.wplocaldocker', 'global' ) ); - } ); -} ); +describe('getGlobalDirectory', () => { + it('returns global/ inside the config directory', () => { + expect(getGlobalDirectory()).toBe(path.join('/fake/home', '.wplocaldocker', 'global')); + }); +}); -describe( 'getDefaults', () => { - it( 'sitesPath is inside the home directory', () => { - expect( getDefaults().sitesPath ).toBe( path.join( '/fake/home', 'wp-local-docker-sites' ) ); - } ); +describe('getDefaults', () => { + it('sitesPath is inside the home directory', () => { + expect(getDefaults().sitesPath).toBe(path.join('/fake/home', 'wp-local-docker-sites')); + }); - it( 'snapshotsPath is inside the home directory', () => { - expect( getDefaults().snapshotsPath ).toBe( path.join( '/fake/home', '.wpsnapshots' ) ); - } ); + it('snapshotsPath is inside the home directory', () => { + expect(getDefaults().snapshotsPath).toBe(path.join('/fake/home', '.wpsnapshots')); + }); - it( 'manageHosts defaults to true', () => { - expect( getDefaults().manageHosts ).toBe( true ); - } ); + it('manageHosts defaults to true', () => { + expect(getDefaults().manageHosts).toBe(true); + }); - it( 'overwriteGlobal defaults to true', () => { - expect( getDefaults().overwriteGlobal ).toBe( true ); - } ); -} ); + it('overwriteGlobal defaults to true', () => { + expect(getDefaults().overwriteGlobal).toBe(true); + }); +}); -describe( 'createProxyConfig', () => { - it( 'replaces #{TRY_PROXY} with the try_files directive', () => { - const result = createProxyConfig( 'https://example.com', '#{TRY_PROXY}' ); - expect( result ).toBe( 'try_files $uri @production;' ); - } ); +describe('createProxyConfig', () => { + it('replaces #{TRY_PROXY} with the try_files directive', () => { + const result = createProxyConfig('https://example.com', '#{TRY_PROXY}'); + expect(result).toBe('try_files $uri @production;'); + }); - it( 'replaces #{PROXY_URL} with a location block', () => { - const result = createProxyConfig( 'https://example.com', '#{PROXY_URL}' ); - expect( result ).toContain( 'location @production {' ); - expect( result ).toContain( '}' ); - } ); + it('replaces #{PROXY_URL} with a location block', () => { + const result = createProxyConfig('https://example.com', '#{PROXY_URL}'); + expect(result).toContain('location @production {'); + expect(result).toContain('}'); + }); - it( 'injects the proxy URL into the proxy_pass directive', () => { - const result = createProxyConfig( 'https://example.com', '#{PROXY_URL}' ); - expect( result ).toContain( 'proxy_pass https://example.com/$uri;' ); - } ); + it('injects the proxy URL into the proxy_pass directive', () => { + const result = createProxyConfig('https://example.com', '#{PROXY_URL}'); + expect(result).toContain('proxy_pass https://example.com/$uri;'); + }); - it( 'replaces both placeholders in the same config string', () => { + it('replaces both placeholders in the same config string', () => { const input = 'before #{TRY_PROXY} middle #{PROXY_URL} after'; - const result = createProxyConfig( 'https://example.com', input ); - expect( result ).toContain( 'try_files $uri @production;' ); - expect( result ).toContain( 'location @production {' ); - expect( result ).toContain( 'after' ); - } ); + const result = createProxyConfig('https://example.com', input); + expect(result).toContain('try_files $uri @production;'); + expect(result).toContain('location @production {'); + expect(result).toContain('after'); + }); - it( 'leaves a string without placeholders unchanged', () => { + it('leaves a string without placeholders unchanged', () => { const input = 'server { listen 80; }'; - expect( createProxyConfig( 'https://example.com', input ) ).toBe( input ); - } ); + expect(createProxyConfig('https://example.com', input)).toBe(input); + }); - it( 'uses different proxy URLs in the generated block', () => { - const result = createProxyConfig( 'https://staging.example.com', '#{PROXY_URL}' ); - expect( result ).toContain( 'proxy_pass https://staging.example.com/$uri;' ); - } ); -} ); + it('uses different proxy URLs in the generated block', () => { + const result = createProxyConfig('https://staging.example.com', '#{PROXY_URL}'); + expect(result).toContain('proxy_pass https://staging.example.com/$uri;'); + }); +}); diff --git a/test/unit/create/inquirer.test.js b/test/unit/create/inquirer.test.js index 41bedee2..e125d4a8 100644 --- a/test/unit/create/inquirer.test.js +++ b/test/unit/create/inquirer.test.js @@ -1,86 +1,86 @@ 'use strict'; -const makeInquirer = require( '../../../src/commands/create/inquirer' ); +const makeInquirer = require('../../../src/commands/create/inquirer'); // Helper: returns a stub prompt that resolves with `answers` immediately. -function stubPrompt( answers ) { - return jest.fn().mockResolvedValue( answers ); +function stubPrompt(answers) { + return jest.fn().mockResolvedValue(answers); } -describe( 'makeInquirer — marshalDomains (single hostname)', () => { - it( 'returns the hostname string when no existing domain is set', async () => { - const prompt = stubPrompt( { hostname: 'docker.test', phpVersion: '8.2' } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( {} ); - expect( result.domain ).toBe( 'docker.test' ); - } ); - - it( 'passes the existing scalar domain through unchanged', async () => { - const prompt = stubPrompt( { phpVersion: '8.2' } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( { domain: 'existing.test' } ); - expect( result.domain ).toBe( 'existing.test' ); - } ); - - it( 'deduplicates when hostname matches a domain already in the defaults array', async () => { - const prompt = stubPrompt( { hostname: 'a.test', phpVersion: '8.2' } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( { domain: 'a.test' } ); +describe('makeInquirer — marshalDomains (single hostname)', () => { + it('returns the hostname string when no existing domain is set', async () => { + const prompt = stubPrompt({ hostname: 'docker.test', phpVersion: '8.2' }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({}); + expect(result.domain).toBe('docker.test'); + }); + + it('passes the existing scalar domain through unchanged', async () => { + const prompt = stubPrompt({ phpVersion: '8.2' }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({ domain: 'existing.test' }); + expect(result.domain).toBe('existing.test'); + }); + + it('deduplicates when hostname matches a domain already in the defaults array', async () => { + const prompt = stubPrompt({ hostname: 'a.test', phpVersion: '8.2' }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({ domain: 'a.test' }); // domain is already 'a.test' (scalar), hostname == same → Set dedupes → single string - expect( result.domain ).toBe( 'a.test' ); - } ); -} ); + expect(result.domain).toBe('a.test'); + }); +}); -describe( 'makeInquirer — marshalDomains (extra hosts)', () => { - it( 'merges extraHosts into the domain array', async () => { - const prompt = stubPrompt( { +describe('makeInquirer — marshalDomains (extra hosts)', () => { + it('merges extraHosts into the domain array', async () => { + const prompt = stubPrompt({ hostname: 'main.test', - extraHosts: [ 'alias1.test', 'alias2.test' ], + extraHosts: ['alias1.test', 'alias2.test'], phpVersion: '8.2', - } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( {} ); - expect( result.domain ).toEqual( [ 'main.test', 'alias1.test', 'alias2.test' ] ); - } ); - - it( 'deduplicates repeated extra hosts', async () => { - const prompt = stubPrompt( { + }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({}); + expect(result.domain).toEqual(['main.test', 'alias1.test', 'alias2.test']); + }); + + it('deduplicates repeated extra hosts', async () => { + const prompt = stubPrompt({ hostname: 'main.test', - extraHosts: [ 'main.test', 'alias.test' ], + extraHosts: ['main.test', 'alias.test'], phpVersion: '8.2', - } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( {} ); + }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({}); // 'main.test' added by hostname, 'main.test' in extraHosts is duped → Set removes it - expect( result.domain ).toEqual( [ 'main.test', 'alias.test' ] ); - } ); + expect(result.domain).toEqual(['main.test', 'alias.test']); + }); - it( 'combines an existing scalar domain with extraHosts', async () => { - const prompt = stubPrompt( { - extraHosts: [ 'extra.test' ], + it('combines an existing scalar domain with extraHosts', async () => { + const prompt = stubPrompt({ + extraHosts: ['extra.test'], phpVersion: '8.2', - } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( { domain: 'existing.test' } ); - expect( result.domain ).toEqual( [ 'existing.test', 'extra.test' ] ); - } ); - - it( 'combines an existing array domain with a new hostname and extraHosts', async () => { - const prompt = stubPrompt( { + }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({ domain: 'existing.test' }); + expect(result.domain).toEqual(['existing.test', 'extra.test']); + }); + + it('combines an existing array domain with a new hostname and extraHosts', async () => { + const prompt = stubPrompt({ hostname: 'new.test', - extraHosts: [ 'extra.test' ], + extraHosts: ['extra.test'], phpVersion: '8.2', - } ); - const inquirer = makeInquirer( { prompt } ); + }); + const inquirer = makeInquirer({ prompt }); // domain is an empty array, so hostname/extraHosts questions fire - const result = await inquirer( { domain: [] } ); - expect( result.domain ).toEqual( [ 'new.test', 'extra.test' ] ); - } ); -} ); - -describe( 'makeInquirer — marshalWordPress (boolean true → {})', () => { - it( 'expands wordpress:true into an object with title/username/password/email', async () => { - const prompt = stubPrompt( { + const result = await inquirer({ domain: [] }); + expect(result.domain).toEqual(['new.test', 'extra.test']); + }); +}); + +describe('makeInquirer — marshalWordPress (boolean true → {})', () => { + it('expands wordpress:true into an object with title/username/password/email', async () => { + const prompt = stubPrompt({ hostname: 'site.test', wordpress: true, wordpressType: 'single', @@ -89,20 +89,20 @@ describe( 'makeInquirer — marshalWordPress (boolean true → {})', () => { password: 'secret', email: 'admin@example.com', phpVersion: '8.2', - } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( {} ); - expect( result.wordpress ).toEqual( { + }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({}); + expect(result.wordpress).toEqual({ type: 'single', title: 'My Site', username: 'admin', password: 'secret', email: 'admin@example.com', - } ); - } ); + }); + }); - it( 'sets purify:true when emptyContent is true', async () => { - const prompt = stubPrompt( { + it('sets purify:true when emptyContent is true', async () => { + const prompt = stubPrompt({ hostname: 'site.test', wordpress: true, wordpressType: 'single', @@ -112,14 +112,14 @@ describe( 'makeInquirer — marshalWordPress (boolean true → {})', () => { email: 'admin@example.com', emptyContent: true, phpVersion: '8.2', - } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( {} ); - expect( result.wordpress.purify ).toBe( true ); - } ); - - it( 'does not set purify when emptyContent is false', async () => { - const prompt = stubPrompt( { + }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({}); + expect(result.wordpress.purify).toBe(true); + }); + + it('does not set purify when emptyContent is false', async () => { + const prompt = stubPrompt({ hostname: 'site.test', wordpress: true, wordpressType: 'single', @@ -129,14 +129,14 @@ describe( 'makeInquirer — marshalWordPress (boolean true → {})', () => { email: 'admin@example.com', emptyContent: false, phpVersion: '8.2', - } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( {} ); - expect( result.wordpress.purify ).toBeUndefined(); - } ); - - it( 'maps wordpressType subdirectory correctly', async () => { - const prompt = stubPrompt( { + }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({}); + expect(result.wordpress.purify).toBeUndefined(); + }); + + it('maps wordpressType subdirectory correctly', async () => { + const prompt = stubPrompt({ hostname: 'ms.test', wordpress: true, wordpressType: 'subdirectory', @@ -145,14 +145,14 @@ describe( 'makeInquirer — marshalWordPress (boolean true → {})', () => { password: 'password', email: 'a@b.com', phpVersion: '8.2', - } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( {} ); - expect( result.wordpress.type ).toBe( 'subdirectory' ); - } ); - - it( 'maps wordpressType subdomain correctly', async () => { - const prompt = stubPrompt( { + }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({}); + expect(result.wordpress.type).toBe('subdirectory'); + }); + + it('maps wordpressType subdomain correctly', async () => { + const prompt = stubPrompt({ hostname: 'ms.test', wordpress: true, wordpressType: 'subdomain', @@ -161,55 +161,55 @@ describe( 'makeInquirer — marshalWordPress (boolean true → {})', () => { password: 'password', email: 'a@b.com', phpVersion: '8.2', - } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( {} ); - expect( result.wordpress.type ).toBe( 'subdomain' ); - } ); -} ); - -describe( 'makeInquirer — marshalWordPress (existing object defaults)', () => { - it( 'merges answers into a pre-existing wordpress object', async () => { - const prompt = stubPrompt( { + }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({}); + expect(result.wordpress.type).toBe('subdomain'); + }); +}); + +describe('makeInquirer — marshalWordPress (existing object defaults)', () => { + it('merges answers into a pre-existing wordpress object', async () => { + const prompt = stubPrompt({ title: 'New Title', username: 'newuser', password: 'newpass', email: 'new@e.com', phpVersion: '8.2', - } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( { + }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({ wordpress: { type: 'single', title: undefined }, - } ); - expect( result.wordpress.title ).toBe( 'New Title' ); - expect( result.wordpress.username ).toBe( 'newuser' ); - } ); -} ); - -describe( 'makeInquirer — top-level field passthrough', () => { - it( 'uses the php version from defaults when already set', async () => { - const prompt = stubPrompt( { hostname: 'site.test' } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( { php: '8.1' } ); - expect( result.php ).toBe( '8.1' ); - } ); - - it( 'uses phpVersion from answers when php not in defaults', async () => { - const prompt = stubPrompt( { hostname: 'site.test', phpVersion: '7.4' } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( {} ); - expect( result.php ).toBe( '7.4' ); - } ); - - it( 'uses the name from defaults when set', async () => { - const prompt = stubPrompt( { hostname: 'site.test', phpVersion: '8.2' } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( { name: 'my-project' } ); - expect( result.name ).toBe( 'my-project' ); - } ); - - it( 'falls back to answers.title for name when not in defaults', async () => { - const prompt = stubPrompt( { + }); + expect(result.wordpress.title).toBe('New Title'); + expect(result.wordpress.username).toBe('newuser'); + }); +}); + +describe('makeInquirer — top-level field passthrough', () => { + it('uses the php version from defaults when already set', async () => { + const prompt = stubPrompt({ hostname: 'site.test' }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({ php: '8.1' }); + expect(result.php).toBe('8.1'); + }); + + it('uses phpVersion from answers when php not in defaults', async () => { + const prompt = stubPrompt({ hostname: 'site.test', phpVersion: '7.4' }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({}); + expect(result.php).toBe('7.4'); + }); + + it('uses the name from defaults when set', async () => { + const prompt = stubPrompt({ hostname: 'site.test', phpVersion: '8.2' }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({ name: 'my-project' }); + expect(result.name).toBe('my-project'); + }); + + it('falls back to answers.title for name when not in defaults', async () => { + const prompt = stubPrompt({ hostname: 'site.test', title: 'Derived Name', wordpress: true, @@ -218,37 +218,45 @@ describe( 'makeInquirer — top-level field passthrough', () => { password: 'pass', email: 'a@b.com', phpVersion: '8.2', - } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( {} ); - expect( result.name ).toBe( 'Derived Name' ); - } ); - - it( 'sets elasticsearch from answers when not in defaults', async () => { - const prompt = stubPrompt( { hostname: 'site.test', phpVersion: '8.2', elasticsearch: true } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( {} ); - expect( result.elasticsearch ).toBe( true ); - } ); - - it( 'defaults elasticsearch to false when neither defaults nor answers set it', async () => { - const prompt = stubPrompt( { hostname: 'site.test', phpVersion: '8.2' } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( {} ); - expect( result.elasticsearch ).toBe( false ); - } ); - - it( 'sets mediaProxy to the proxy answer when present', async () => { - const prompt = stubPrompt( { hostname: 'site.test', phpVersion: '8.2', proxy: 'http://proxy.example.com' } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( {} ); - expect( result.mediaProxy ).toBe( 'http://proxy.example.com' ); - } ); - - it( 'sets mediaProxy to false when proxy answer is absent', async () => { - const prompt = stubPrompt( { hostname: 'site.test', phpVersion: '8.2' } ); - const inquirer = makeInquirer( { prompt } ); - const result = await inquirer( {} ); - expect( result.mediaProxy ).toBe( false ); - } ); -} ); + }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({}); + expect(result.name).toBe('Derived Name'); + }); + + it('sets elasticsearch from answers when not in defaults', async () => { + const prompt = stubPrompt({ + hostname: 'site.test', + phpVersion: '8.2', + elasticsearch: true, + }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({}); + expect(result.elasticsearch).toBe(true); + }); + + it('defaults elasticsearch to false when neither defaults nor answers set it', async () => { + const prompt = stubPrompt({ hostname: 'site.test', phpVersion: '8.2' }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({}); + expect(result.elasticsearch).toBe(false); + }); + + it('sets mediaProxy to the proxy answer when present', async () => { + const prompt = stubPrompt({ + hostname: 'site.test', + phpVersion: '8.2', + proxy: 'http://proxy.example.com', + }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({}); + expect(result.mediaProxy).toBe('http://proxy.example.com'); + }); + + it('sets mediaProxy to false when proxy answer is absent', async () => { + const prompt = stubPrompt({ hostname: 'site.test', phpVersion: '8.2' }); + const inquirer = makeInquirer({ prompt }); + const result = await inquirer({}); + expect(result.mediaProxy).toBe(false); + }); +}); diff --git a/test/unit/env-utils.test.js b/test/unit/env-utils.test.js index 5ca80e84..cdcd5282 100644 --- a/test/unit/env-utils.test.js +++ b/test/unit/env-utils.test.js @@ -1,59 +1,59 @@ 'use strict'; -const { envSlug, createDefaultProxy } = require( '../../src/env-utils' ); - -describe( 'envSlug', () => { - it( 'returns a plain lowercase word unchanged', () => { - expect( envSlug( 'mysite' ) ).toBe( 'mysite' ); - } ); - - it( 'converts spaces to hyphens', () => { - expect( envSlug( 'hello world' ) ).toBe( 'hello-world' ); - } ); - - it( 'lowercases uppercase input', () => { - expect( envSlug( 'MySite' ) ).toBe( 'my-site' ); - } ); - - it( 'converts dots to hyphens', () => { - expect( envSlug( 'my.site.com' ) ).toBe( 'my-site-com' ); - } ); - - it( 'converts underscores to hyphens', () => { - expect( envSlug( 'test_env' ) ).toBe( 'test-env' ); - } ); - - it( 'strips leading and trailing spaces', () => { - expect( envSlug( ' leading spaces ' ) ).toBe( 'leading-spaces' ); - } ); - - it( 'preserves already-slugified input', () => { - expect( envSlug( 'hello-world' ) ).toBe( 'hello-world' ); - } ); -} ); - -describe( 'createDefaultProxy', () => { - it( 'prepends http:// to a plain hostname', () => { - expect( createDefaultProxy( 'example.com' ) ).toBe( 'http://example.com' ); - } ); - - it( 'replaces a non-com TLD with com', () => { - expect( createDefaultProxy( 'example.net' ) ).toBe( 'http://example.com' ); - } ); - - it( 'appends .com when there is no dot in the hostname', () => { - expect( createDefaultProxy( 'example' ) ).toBe( 'http://example.com' ); - } ); - - it( 'replaces only the last TLD segment for a subdomain', () => { - expect( createDefaultProxy( 'sub.example.net' ) ).toBe( 'http://sub.example.com' ); - } ); - - it( 'strips a trailing slash from the value', () => { - expect( createDefaultProxy( 'example.com/' ) ).toBe( 'http://example.com' ); - } ); - - it( 'strips leading and trailing slashes from the value', () => { - expect( createDefaultProxy( '/example.com/' ) ).toBe( 'http://example.com' ); - } ); -} ); +const { envSlug, createDefaultProxy } = require('../../src/env-utils'); + +describe('envSlug', () => { + it('returns a plain lowercase word unchanged', () => { + expect(envSlug('mysite')).toBe('mysite'); + }); + + it('converts spaces to hyphens', () => { + expect(envSlug('hello world')).toBe('hello-world'); + }); + + it('lowercases uppercase input', () => { + expect(envSlug('MySite')).toBe('my-site'); + }); + + it('converts dots to hyphens', () => { + expect(envSlug('my.site.com')).toBe('my-site-com'); + }); + + it('converts underscores to hyphens', () => { + expect(envSlug('test_env')).toBe('test-env'); + }); + + it('strips leading and trailing spaces', () => { + expect(envSlug(' leading spaces ')).toBe('leading-spaces'); + }); + + it('preserves already-slugified input', () => { + expect(envSlug('hello-world')).toBe('hello-world'); + }); +}); + +describe('createDefaultProxy', () => { + it('prepends http:// to a plain hostname', () => { + expect(createDefaultProxy('example.com')).toBe('http://example.com'); + }); + + it('replaces a non-com TLD with com', () => { + expect(createDefaultProxy('example.net')).toBe('http://example.com'); + }); + + it('appends .com when there is no dot in the hostname', () => { + expect(createDefaultProxy('example')).toBe('http://example.com'); + }); + + it('replaces only the last TLD segment for a subdomain', () => { + expect(createDefaultProxy('sub.example.net')).toBe('http://sub.example.com'); + }); + + it('strips a trailing slash from the value', () => { + expect(createDefaultProxy('example.com/')).toBe('http://example.com'); + }); + + it('strips leading and trailing slashes from the value', () => { + expect(createDefaultProxy('/example.com/')).toBe('http://example.com'); + }); +}); diff --git a/test/unit/helpers.test.js b/test/unit/helpers.test.js index da49a40d..ec028f9f 100644 --- a/test/unit/helpers.test.js +++ b/test/unit/helpers.test.js @@ -1,73 +1,73 @@ 'use strict'; -const { unleadingslashit, untrailingslashit, removeEndSlashes } = require( '../../src/helpers' ); - -describe( 'unleadingslashit', () => { - it( 'removes a leading slash', () => { - expect( unleadingslashit( '/foo' ) ).toBe( 'foo' ); - } ); - - it( 'removes multiple leading slashes', () => { - expect( unleadingslashit( '///foo' ) ).toBe( 'foo' ); - } ); - - it( 'leaves a string with no leading slash unchanged', () => { - expect( unleadingslashit( 'foo' ) ).toBe( 'foo' ); - } ); - - it( 'leaves an empty string unchanged', () => { - expect( unleadingslashit( '' ) ).toBe( '' ); - } ); - - it( 'does not remove a trailing slash', () => { - expect( unleadingslashit( 'foo/' ) ).toBe( 'foo/' ); - } ); -} ); - -describe( 'untrailingslashit', () => { - it( 'removes a trailing slash', () => { - expect( untrailingslashit( 'foo/' ) ).toBe( 'foo' ); - } ); - - it( 'removes only one trailing slash', () => { - expect( untrailingslashit( 'foo//' ) ).toBe( 'foo/' ); - } ); - - it( 'leaves a string with no trailing slash unchanged', () => { - expect( untrailingslashit( 'foo' ) ).toBe( 'foo' ); - } ); - - it( 'leaves an empty string unchanged', () => { - expect( untrailingslashit( '' ) ).toBe( '' ); - } ); - - it( 'does not remove a leading slash', () => { - expect( untrailingslashit( '/foo' ) ).toBe( '/foo' ); - } ); -} ); - -describe( 'removeEndSlashes', () => { - it( 'removes both leading and trailing slashes', () => { - expect( removeEndSlashes( '/foo/' ) ).toBe( 'foo' ); - } ); - - it( 'removes multiple leading slashes', () => { - expect( removeEndSlashes( '///foo' ) ).toBe( 'foo' ); - } ); - - it( 'removes a trailing slash but only one', () => { - expect( removeEndSlashes( 'foo//' ) ).toBe( 'foo/' ); - } ); - - it( 'leaves a string with no slashes unchanged', () => { - expect( removeEndSlashes( 'foo' ) ).toBe( 'foo' ); - } ); - - it( 'leaves an empty string unchanged', () => { - expect( removeEndSlashes( '' ) ).toBe( '' ); - } ); - - it( 'handles slashes on both ends with multiple leading slashes', () => { - expect( removeEndSlashes( '///foo/' ) ).toBe( 'foo' ); - } ); -} ); +const { unleadingslashit, untrailingslashit, removeEndSlashes } = require('../../src/helpers'); + +describe('unleadingslashit', () => { + it('removes a leading slash', () => { + expect(unleadingslashit('/foo')).toBe('foo'); + }); + + it('removes multiple leading slashes', () => { + expect(unleadingslashit('///foo')).toBe('foo'); + }); + + it('leaves a string with no leading slash unchanged', () => { + expect(unleadingslashit('foo')).toBe('foo'); + }); + + it('leaves an empty string unchanged', () => { + expect(unleadingslashit('')).toBe(''); + }); + + it('does not remove a trailing slash', () => { + expect(unleadingslashit('foo/')).toBe('foo/'); + }); +}); + +describe('untrailingslashit', () => { + it('removes a trailing slash', () => { + expect(untrailingslashit('foo/')).toBe('foo'); + }); + + it('removes only one trailing slash', () => { + expect(untrailingslashit('foo//')).toBe('foo/'); + }); + + it('leaves a string with no trailing slash unchanged', () => { + expect(untrailingslashit('foo')).toBe('foo'); + }); + + it('leaves an empty string unchanged', () => { + expect(untrailingslashit('')).toBe(''); + }); + + it('does not remove a leading slash', () => { + expect(untrailingslashit('/foo')).toBe('/foo'); + }); +}); + +describe('removeEndSlashes', () => { + it('removes both leading and trailing slashes', () => { + expect(removeEndSlashes('/foo/')).toBe('foo'); + }); + + it('removes multiple leading slashes', () => { + expect(removeEndSlashes('///foo')).toBe('foo'); + }); + + it('removes a trailing slash but only one', () => { + expect(removeEndSlashes('foo//')).toBe('foo/'); + }); + + it('leaves a string with no slashes unchanged', () => { + expect(removeEndSlashes('foo')).toBe('foo'); + }); + + it('leaves an empty string unchanged', () => { + expect(removeEndSlashes('')).toBe(''); + }); + + it('handles slashes on both ends with multiple leading slashes', () => { + expect(removeEndSlashes('///foo/')).toBe('foo'); + }); +}); diff --git a/test/unit/prompt-validators.test.js b/test/unit/prompt-validators.test.js index 346bbd3b..521183ef 100644 --- a/test/unit/prompt-validators.test.js +++ b/test/unit/prompt-validators.test.js @@ -1,119 +1,124 @@ 'use strict'; -const { validateNotEmpty, validateBool, parseHostname, parseProxyUrl } = require( '../../src/prompt-validators' ); - -describe( 'validateNotEmpty', () => { - it( 'returns true for a non-empty string', () => { - expect( validateNotEmpty( 'hello' ) ).toBe( true ); - } ); - - it( 'returns an error message for an empty string', () => { - expect( validateNotEmpty( '' ) ).toBe( 'This field is required' ); - } ); - - it( 'returns an error message for a whitespace-only string', () => { - expect( validateNotEmpty( ' ' ) ).toBe( 'This field is required' ); - } ); - - it( 'returns true for a string with leading/trailing whitespace around content', () => { - expect( validateNotEmpty( ' hello ' ) ).toBe( true ); - } ); -} ); - -describe( 'validateBool', () => { - it( 'returns "true" for "y"', () => { - expect( validateBool( 'y' ) ).toBe( 'true' ); - } ); - - it( 'returns "true" for "Y" (case-insensitive)', () => { - expect( validateBool( 'Y' ) ).toBe( 'true' ); - } ); - - it( 'returns "true" for "yes"', () => { - expect( validateBool( 'yes' ) ).toBe( 'true' ); - } ); - - it( 'returns "true" for "YES" (case-insensitive)', () => { - expect( validateBool( 'YES' ) ).toBe( 'true' ); - } ); - - it( 'returns "false" for "n"', () => { - expect( validateBool( 'n' ) ).toBe( 'false' ); - } ); - - it( 'returns "false" for "no"', () => { - expect( validateBool( 'no' ) ).toBe( 'false' ); - } ); - - it( 'returns "false" for "NO" (case-insensitive)', () => { - expect( validateBool( 'NO' ) ).toBe( 'false' ); - } ); - - it( 'returns the original string for an unrecognized value', () => { - expect( validateBool( 'maybe' ) ).toBe( 'maybe' ); - } ); - - it( 'passes through a non-string value unchanged', () => { - expect( validateBool( true ) ).toBe( true ); - } ); - - it( 'passes through a numeric value unchanged', () => { - expect( validateBool( 42 ) ).toBe( 42 ); - } ); -} ); - -describe( 'parseHostname', () => { - it( 'strips an http:// prefix', () => { - expect( parseHostname( 'http://example.com' ) ).toBe( 'example.com' ); - } ); - - it( 'strips an https:// prefix', () => { - expect( parseHostname( 'https://example.com' ) ).toBe( 'example.com' ); - } ); - - it( 'strips an HTTP:// prefix (case-insensitive)', () => { - expect( parseHostname( 'HTTP://example.com' ) ).toBe( 'example.com' ); - } ); - - it( 'removes a path component, keeping only the hostname', () => { - expect( parseHostname( 'example.com/some/path' ) ).toBe( 'example.com' ); - } ); - - it( 'removes both protocol and path', () => { - expect( parseHostname( 'https://example.com/some/path' ) ).toBe( 'example.com' ); - } ); - - it( 'removes spaces from the value', () => { - expect( parseHostname( 'example .com' ) ).toBe( 'example.com' ); - } ); - - it( 'returns a plain hostname unchanged', () => { - expect( parseHostname( 'example.com' ) ).toBe( 'example.com' ); - } ); -} ); - -describe( 'parseProxyUrl', () => { - it( 'adds http:// when no protocol is present and value is longer than 3 chars', () => { - expect( parseProxyUrl( 'example.com' ) ).toBe( 'http://example.com' ); - } ); - - it( 'does not add a protocol when one is already present (http)', () => { - expect( parseProxyUrl( 'http://example.com' ) ).toBe( 'http://example.com' ); - } ); - - it( 'does not add a protocol when one is already present (https)', () => { - expect( parseProxyUrl( 'https://example.com' ) ).toBe( 'https://example.com' ); - } ); - - it( 'does not modify a value with length <= 3', () => { - expect( parseProxyUrl( 'foo' ) ).toBe( 'foo' ); - } ); - - it( 'removes a trailing slash', () => { - expect( parseProxyUrl( 'http://example.com/' ) ).toBe( 'http://example.com' ); - } ); - - it( 'adds protocol and removes trailing slash together', () => { - expect( parseProxyUrl( 'example.com/' ) ).toBe( 'http://example.com' ); - } ); -} ); +const { + validateNotEmpty, + validateBool, + parseHostname, + parseProxyUrl, +} = require('../../src/prompt-validators'); + +describe('validateNotEmpty', () => { + it('returns true for a non-empty string', () => { + expect(validateNotEmpty('hello')).toBe(true); + }); + + it('returns an error message for an empty string', () => { + expect(validateNotEmpty('')).toBe('This field is required'); + }); + + it('returns an error message for a whitespace-only string', () => { + expect(validateNotEmpty(' ')).toBe('This field is required'); + }); + + it('returns true for a string with leading/trailing whitespace around content', () => { + expect(validateNotEmpty(' hello ')).toBe(true); + }); +}); + +describe('validateBool', () => { + it('returns "true" for "y"', () => { + expect(validateBool('y')).toBe('true'); + }); + + it('returns "true" for "Y" (case-insensitive)', () => { + expect(validateBool('Y')).toBe('true'); + }); + + it('returns "true" for "yes"', () => { + expect(validateBool('yes')).toBe('true'); + }); + + it('returns "true" for "YES" (case-insensitive)', () => { + expect(validateBool('YES')).toBe('true'); + }); + + it('returns "false" for "n"', () => { + expect(validateBool('n')).toBe('false'); + }); + + it('returns "false" for "no"', () => { + expect(validateBool('no')).toBe('false'); + }); + + it('returns "false" for "NO" (case-insensitive)', () => { + expect(validateBool('NO')).toBe('false'); + }); + + it('returns the original string for an unrecognized value', () => { + expect(validateBool('maybe')).toBe('maybe'); + }); + + it('passes through a non-string value unchanged', () => { + expect(validateBool(true)).toBe(true); + }); + + it('passes through a numeric value unchanged', () => { + expect(validateBool(42)).toBe(42); + }); +}); + +describe('parseHostname', () => { + it('strips an http:// prefix', () => { + expect(parseHostname('http://example.com')).toBe('example.com'); + }); + + it('strips an https:// prefix', () => { + expect(parseHostname('https://example.com')).toBe('example.com'); + }); + + it('strips an HTTP:// prefix (case-insensitive)', () => { + expect(parseHostname('HTTP://example.com')).toBe('example.com'); + }); + + it('removes a path component, keeping only the hostname', () => { + expect(parseHostname('example.com/some/path')).toBe('example.com'); + }); + + it('removes both protocol and path', () => { + expect(parseHostname('https://example.com/some/path')).toBe('example.com'); + }); + + it('removes spaces from the value', () => { + expect(parseHostname('example .com')).toBe('example.com'); + }); + + it('returns a plain hostname unchanged', () => { + expect(parseHostname('example.com')).toBe('example.com'); + }); +}); + +describe('parseProxyUrl', () => { + it('adds http:// when no protocol is present and value is longer than 3 chars', () => { + expect(parseProxyUrl('example.com')).toBe('http://example.com'); + }); + + it('does not add a protocol when one is already present (http)', () => { + expect(parseProxyUrl('http://example.com')).toBe('http://example.com'); + }); + + it('does not add a protocol when one is already present (https)', () => { + expect(parseProxyUrl('https://example.com')).toBe('https://example.com'); + }); + + it('does not modify a value with length <= 3', () => { + expect(parseProxyUrl('foo')).toBe('foo'); + }); + + it('removes a trailing slash', () => { + expect(parseProxyUrl('http://example.com/')).toBe('http://example.com'); + }); + + it('adds protocol and removes trailing slash together', () => { + expect(parseProxyUrl('example.com/')).toBe('http://example.com'); + }); +}); From ec4ea27cbe9ea81517007ef387037bc59932fe11 Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Sun, 7 Jun 2026 08:28:50 -0300 Subject: [PATCH 14/16] ci: run lint, format check and tests on push/PR to develop and main Replace the push-on-everything Node CI with a workflow triggered by pushes and pull requests targeting develop or main. It installs deps, runs ESLint, prettier --check, and the full test suite across Node 20, 22 and 26. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/nodejs.yml | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index c6b94eaf..919a62ae 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -2,20 +2,34 @@ name: Node CI on: push: + branches: [develop, main] + pull_request: + branches: [develop, main] + +permissions: + contents: read jobs: checks: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [20, 22, 26] steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 with: - node-version: '26' - - name: install dependencies + node-version: ${{ matrix.node-version }} + cache: npm + - name: Install dependencies run: npm ci env: CI: true - - name: run linter + - name: Lint (ESLint) run: npm run lint --silent - - name: run tests + - name: Format check (Prettier) + run: npm run format:check + - name: Run tests run: npm test From 342f2987b5f35f031daf8ebeec15db1dafe5708d Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Sun, 7 Jun 2026 08:29:01 -0300 Subject: [PATCH 15/16] chore: set Node version to 26 in .nvmrc Align the pinned dev Node version with the local environment and the CI matrix (was 18, which also conflicted with engines >=20). Co-Authored-By: Claude Opus 4.8 --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 3c032078..6f4247a6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 +26 From b4b92d1a499b197a35c9af131251d6068b8389d3 Mon Sep 17 00:00:00 2001 From: Ramon Ahnert Date: Sun, 7 Jun 2026 09:14:48 -0300 Subject: [PATCH 16/16] build: drop @10up/eslint-config and babel-eslint Remove the 10up-authored ESLint shareable config and its babel-eslint parser. Extend eslint:recommended directly (with explicit parserOptions/env so the default espree parser is used) and inline the worthwhile code-quality rules the 10up config provided (prefer-const, prefer-template, no-var, no-alert). Prettier continues to own formatting via eslint-config-prettier. Co-Authored-By: Claude Opus 4.8 --- .eslintrc.json | 16 +++++++++++---- package-lock.json | 51 +---------------------------------------------- package.json | 2 -- 3 files changed, 13 insertions(+), 56 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 2d00b8a0..c1340d38 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,12 @@ { - "extends": ["@10up/eslint-config", "prettier"], + "root": true, + "extends": ["eslint:recommended", "prettier"], + "parserOptions": { + "ecmaVersion": 2021, + "sourceType": "script" + }, "env": { - "es6": true, + "es2021": true, "node": true, "jest": true }, @@ -12,19 +17,22 @@ "allow": ["depends_on", "mem_limit", "mem_reservation", "cap_add"] } ], + "no-alert": "warn", + "no-console": "off", "no-constant-condition": [ "error", { "checkLoops": false } ], - "no-console": "off", "no-empty": [ "error", { "allowEmptyCatch": true } ], + "no-var": "warn", + "prefer-const": "error", "prefer-destructuring": [ "error", { @@ -32,7 +40,7 @@ "object": true } ], - "require-jsdoc": "off", + "prefer-template": "error", "yoda": ["error", "never"] } } diff --git a/package-lock.json b/package-lock.json index 1895e846..f6d3442a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,8 +43,6 @@ "wpdocker-hosts": "hosts.js" }, "devDependencies": { - "@10up/eslint-config": "^1.0.9", - "babel-eslint": "^10.0.3", "eslint": "^7.32.0", "eslint-config-prettier": "^9.1.2", "husky": "^9.1.7", @@ -53,15 +51,9 @@ "prettier": "^3.8.3" }, "engines": { - "node": ">=18" + "node": ">=20" } }, - "node_modules/@10up/eslint-config": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@10up/eslint-config/-/eslint-config-1.0.9.tgz", - "integrity": "sha512-gtDQpEIYFpaWnM9ObbY6yCeP2lsfbIqnZPxHjVrq3vv2zDShlMrGUQxUYBp+IC22dixCI2iYNIgSBkRsWC/8qA==", - "dev": true - }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -1545,27 +1537,6 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, - "node_modules/babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "deprecated": "babel-eslint is now @babel/eslint-parser. This package will no longer receive updates.", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - }, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "eslint": ">= 4.12.1" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -6398,12 +6369,6 @@ } }, "dependencies": { - "@10up/eslint-config": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@10up/eslint-config/-/eslint-config-1.0.9.tgz", - "integrity": "sha512-gtDQpEIYFpaWnM9ObbY6yCeP2lsfbIqnZPxHjVrq3vv2zDShlMrGUQxUYBp+IC22dixCI2iYNIgSBkRsWC/8qA==", - "dev": true - }, "@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -7547,20 +7512,6 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, - "babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - } - }, "babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", diff --git a/package.json b/package.json index ebb76bfd..2ed6f629 100644 --- a/package.json +++ b/package.json @@ -87,8 +87,6 @@ "yargs": "^16.2.0" }, "devDependencies": { - "@10up/eslint-config": "^1.0.9", - "babel-eslint": "^10.0.3", "eslint": "^7.32.0", "eslint-config-prettier": "^9.1.2", "husky": "^9.1.7",