From 882a93dea59f7e850ab038218849838f81061373 Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Fri, 5 Feb 2021 23:45:48 +0100 Subject: [PATCH] Add linter and minor bug fixes (#107) --- .github/workflows/workflow.yml | 20 +- package-lock.json | 594 ++++++++++++++++++ package.json | 4 +- source/.eslintrc.yml | 158 +++++ source/app/action/index.mjs | 390 ++++++------ source/app/metrics/index.mjs | 10 +- source/app/metrics/metadata.mjs | 41 +- source/app/metrics/setup.mjs | 50 +- source/app/metrics/utils.mjs | 49 +- source/app/mocks/.eslintrc.yml | 5 + source/app/mocks/api/axios/get/lastfm.mjs | 6 +- source/app/mocks/api/axios/get/pagespeed.mjs | 24 +- source/app/mocks/api/axios/get/spotify.mjs | 22 +- source/app/mocks/api/axios/get/twitter.mjs | 14 +- source/app/mocks/api/axios/get/wakatime.mjs | 14 +- source/app/mocks/api/axios/post/anilist.mjs | 69 +- source/app/mocks/api/axios/post/spotify.mjs | 8 +- .../api/github/graphql/base.repositories.mjs | 24 +- .../api/github/graphql/base.repository.mjs | 14 +- .../mocks/api/github/graphql/base.user.mjs | 26 +- .../api/github/graphql/gists.default.mjs | 26 +- .../github/graphql/isocalendar.calendar.mjs | 20 +- .../api/github/graphql/people.default.mjs | 20 +- .../api/github/graphql/people.repository.mjs | 24 +- .../api/github/graphql/people.sponsors.mjs | 24 +- .../github/graphql/projects.repository.mjs | 18 +- .../api/github/graphql/projects.user.mjs | 20 +- .../api/github/graphql/stargazers.default.mjs | 20 +- .../api/github/graphql/stars.default.mjs | 22 +- .../listEventsForAuthenticatedUser.mjs | 44 +- .../mocks/api/github/rest/rateLimit/get.mjs | 8 +- .../rest/repos/getContributorsStats.mjs | 14 +- .../mocks/api/github/rest/repos/getViews.mjs | 10 +- .../api/github/rest/repos/listCommits.mjs | 20 +- .../github/rest/repos/listContributors.mjs | 10 +- source/app/mocks/api/github/rest/request.mjs | 16 +- .../api/github/rest/users/getByUsername.mjs | 10 +- source/app/mocks/index.mjs | 25 +- source/app/web/index.mjs | 4 +- source/app/web/instance.mjs | 46 +- source/plugins/activity/index.mjs | 2 +- source/plugins/activity/metadata.yml | 3 +- source/plugins/anilist/index.mjs | 8 +- source/plugins/anilist/metadata.yml | 1 + source/plugins/base/index.mjs | 15 +- source/plugins/core/index.mjs | 19 +- source/plugins/followup/index.mjs | 30 +- source/plugins/gists/index.mjs | 4 +- source/plugins/habits/index.mjs | 26 +- source/plugins/isocalendar/index.mjs | 14 +- source/plugins/languages/index.mjs | 4 +- source/plugins/languages/metadata.yml | 4 + source/plugins/lines/index.mjs | 4 +- source/plugins/music/README.md | 2 +- source/plugins/music/index.mjs | 84 ++- source/plugins/music/metadata.yml | 3 + source/plugins/music/tests.yml | 1 + source/plugins/pagespeed/index.mjs | 4 +- source/plugins/people/index.mjs | 4 +- source/plugins/people/metadata.yml | 1 + source/plugins/posts/index.mjs | 6 +- source/plugins/projects/index.mjs | 4 +- source/plugins/projects/metadata.yml | 1 + source/plugins/stargazers/index.mjs | 4 +- source/plugins/stars/index.mjs | 2 +- source/plugins/topics/index.mjs | 4 +- source/plugins/traffic/index.mjs | 6 +- source/plugins/tweets/index.mjs | 13 +- source/plugins/wakatime/index.mjs | 7 +- source/templates/classic/partials/music.ejs | 8 +- source/templates/classic/style.css | 3 +- source/templates/classic/template.mjs | 6 +- source/templates/repository/template.mjs | 10 +- source/templates/terminal/template.mjs | 6 +- 74 files changed, 1544 insertions(+), 712 deletions(-) create mode 100644 source/.eslintrc.yml create mode 100644 source/app/mocks/.eslintrc.yml diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 147800e1..ac1da146 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -24,11 +24,27 @@ jobs: - name: Run tests run: docker run --workdir=/metrics --entrypoint="" lowlighter/metrics:${{ github.head_ref || 'master' }} npm test + # Run linter to ensure new code respect coding rules + lint: + name: Apply linter + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Setup NodeJS + uses: actions/setup-node@v2 + with: + node-version: 15 + - name: Setup metrics + run: npm ci + - name: Run linter + run: npm run linter + # Run CodeQL on branch analyze: name: Analyze code runs-on: ubuntu-latest - needs: [ build ] + needs: [ lint ] steps: - name: Checkout repository uses: actions/checkout@v2 @@ -44,7 +60,7 @@ jobs: docker-master: name: Publish master to GitHub registry runs-on: ubuntu-latest - needs: [ build ] + needs: [ build, lint ] if: github.event_name == 'push' && github.ref == 'refs/heads/master' steps: - name: Checkout repository diff --git a/package-lock.json b/package-lock.json index 2ca05731..fa8fbc0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -416,6 +416,66 @@ "minimist": "^1.2.0" } }, + "@eslint/eslintrc": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz", + "integrity": "sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.20", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "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", @@ -1227,6 +1287,12 @@ "acorn-walk": "^7.1.1" } }, + "acorn-jsx": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "dev": true + }, "acorn-walk": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", @@ -1250,6 +1316,12 @@ "uri-js": "^4.2.2" } }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, "ansi-escapes": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", @@ -1365,6 +1437,12 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, "async": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", @@ -2261,6 +2339,15 @@ "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", "dev": true }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "dom-serializer": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", @@ -2356,6 +2443,15 @@ "once": "^1.4.0" } }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, "entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -2424,11 +2520,322 @@ "source-map": "~0.6.1" } }, + "eslint": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.19.0.tgz", + "integrity": "sha512-CGlMgJY56JZ9ZSYhJuhow61lMPPjUzWmChFya71Z/jilVos7mR/jPgaEfVGgMBY5DshbKdG8Ezb8FDCHcoMEMg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.3.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^6.0.0", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.20", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.4", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", @@ -2771,6 +3178,15 @@ "pend": "~1.2.0" } }, + "file-entry-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz", + "integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, "filelist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.1.tgz", @@ -2811,6 +3227,22 @@ "path-exists": "^4.0.0" } }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz", + "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", + "dev": true + }, "follow-redirects": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", @@ -2889,6 +3321,12 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -3007,6 +3445,15 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -3214,6 +3661,12 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, "ignore-walk": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", @@ -3231,6 +3684,24 @@ "node-fetch": "^2.6.0" } }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, "import-local": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", @@ -3390,6 +3861,12 @@ "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "dev": true }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "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", @@ -3402,6 +3879,15 @@ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, "is-negative-zero": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", @@ -4935,6 +5421,12 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -5680,6 +6172,15 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, "parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -6025,6 +6526,12 @@ "safe-regex": "^1.1.0" } }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -6133,6 +6640,12 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -6522,6 +7035,43 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -6938,6 +7488,38 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "table": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/table/-/table-6.0.7.tgz", + "integrity": "sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g==", + "dev": true, + "requires": { + "ajv": "^7.0.2", + "lodash": "^4.17.20", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0" + }, + "dependencies": { + "ajv": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.4.tgz", + "integrity": "sha512-xzzzaqgEQfmuhbhAoqjJ8T/1okb6gAzXn/eQRNpAN1AEUoHJTNF9xCDRTtf/s3SKldtZfa+RJeTs+BQq+eZ/sw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, "tar": { "version": "4.4.13", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", @@ -7017,6 +7599,12 @@ "minimatch": "^3.0.4" } }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, "throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", @@ -7316,6 +7904,12 @@ "dev": true, "optional": true }, + "v8-compile-cache": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", + "dev": true + }, "v8-to-istanbul": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.0.tgz", diff --git a/package.json b/package.json index 327eb9cd..d32fdbfa 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "test": "npx jest", "index": "node .github/index.mjs", "upgrade": "npm install @actions/core@latest @actions/github@latest @octokit/graphql@latest @octokit/rest@latest axios@latest compression@latest ejs@latest express@latest express-rate-limit@latest faker@latest image-to-base64@latest js-yaml@latest memory-cache@latest prismjs@latest puppeteer@latest svgo@latest vue@latest jest@latest libxmljs@latest", - "quickstart": "node .github/quickstart/index.mjs" + "quickstart": "node .github/quickstart/index.mjs", + "linter": "npx eslint source/**/*.mjs" }, "repository": { "type": "git", @@ -43,6 +44,7 @@ "vue-prism-component": "^1.2.0" }, "devDependencies": { + "eslint": "^7.19.0", "jest": "^26.6.3", "libxmljs": "^0.19.7" }, diff --git a/source/.eslintrc.yml b/source/.eslintrc.yml new file mode 100644 index 00000000..fe8a44fd --- /dev/null +++ b/source/.eslintrc.yml @@ -0,0 +1,158 @@ +# Use recommended rules +extends: eslint:recommended + +# Environment +env: + node: yes + es2021: yes +parserOptions: + ecmaVersion: 2021 + sourceType: module + +# Globally defined variables +globals: + # Puppeteer variables + document: readonly + window: readonly + XMLSerializer: readonly + +# Rules +rules: + + # Avoid useless statements + no-unused-vars: [error, {argsIgnorePattern: "^_"}] + no-unused-expressions: error + no-return-await: error + no-empty-function: error + no-useless-call: error + no-useless-constructor: error + no-useless-concat: error + no-useless-computed-key: error + no-useless-backreference: error + no-self-compare: error + no-extra-label: error + no-undef-init: error + + # Avoid visual pollution + semi: [error, never] + semi-spacing: error + semi-style: [error, first] + curly: [error, multi-or-nest] + dot-notation: error + + # Avoid confusing code + no-label-var: error + no-bitwise: error + new-cap: error + new-parens: error + func-name-matching: error + no-extend-native: error + no-extra-bind: error + + # Avoid deprecated or legacy JavaScript + no-var: error + no-caller: error + no-alert: error + no-script-url: error + no-eval: error + no-implied-eval: error + no-implicit-globals: error + no-proto: error + no-iterator: error + no-new-object: error + + # Code integrity + no-unsafe-optional-chaining: error + no-duplicate-imports: error + no-promise-executor-return: error + eqeqeq: error + + # Code simplicity + max-depth: [error, 10] + max-nested-callbacks: error + max-params: [error, 3] + max-statements-per-line: error + newline-per-chained-call: [error, {ignoreChainWithDepth: 6}] + object-shorthand: error + + # Code readability + default-case-last: error + default-param-last: error + no-else-return: error + no-lonely-if: error + no-multiple-empty-lines: error + no-multi-str: error + no-multi-assign: error + no-inline-comments: error + yoda: error + max-classes-per-file: [error, 1] + grouped-accessor-pairs: error + + # Prefer using new syntax and features + prefer-regex-literals: error + prefer-named-capture-group: error + prefer-arrow-callback: error + prefer-destructuring: error + prefer-numeric-literals: error + prefer-exponentiation-operator: error + prefer-spread: error + prefer-object-spread: error + prefer-template: error + + # Allow additional features + no-ex-assign: off + no-unsafe-finally: off + + # ========================================================================= + # Rules below are really motivated by + + # Code style (general rules) + no-tabs: error + no-trailing-spaces: error + brace-style: [error, stroustrup] + comma-dangle: [error, always-multiline] + comma-style: error + sort-vars: error + + # Coding style (quoting rules) + quote-props: [error, as-needed, {numbers: true}] + quotes: [error, double, {avoidEscape: true}] + template-curly-spacing: error + + # Coding style (comments rules) + line-comment-position: error + capitalized-comments: error + multiline-comment-style: error + + # Coding style (spacing rules) + block-spacing: [error, always] + comma-spacing: error + func-call-spacing: error + arrow-spacing: error + generator-star-spacing: error + object-curly-spacing: [error, never] + rest-spread-spacing: error + key-spacing: [error, {afterColon: false}] + computed-property-spacing: error + switch-colon-spacing: [error, {after: false}] + array-bracket-spacing: [error, never] + no-whitespace-before-property: error + space-before-function-paren: [error, never] + space-in-parens: error + spaced-comment: [error, never] + lines-between-class-members: error + keyword-spacing: error + array-bracket-newline: [error, consistent] + array-element-newline: [error, consistent] + + # Coding style (function rules) + no-new: error + no-new-func: error + no-new-wrappers: error + func-names: [error, never] + function-call-argument-newline: [error, never] + function-paren-newline: [error, never] + no-extra-parens: [error, functions] + arrow-body-style: [error, as-needed] + arrow-parens: [error, as-needed] + implicit-arrow-linebreak: error diff --git a/source/app/action/index.mjs b/source/app/action/index.mjs index 587d8ff9..99d393f6 100644 --- a/source/app/action/index.mjs +++ b/source/app/action/index.mjs @@ -5,7 +5,7 @@ import setup from "../metrics/setup.mjs" import mocks from "../mocks/index.mjs" import metrics from "../metrics/index.mjs" - process.on("unhandledRejection", error => { throw error }) + process.on("unhandledRejection", error => { throw error }) //eslint-disable-line max-statements-per-line, brace-style //Debug message buffer let DEBUG = true @@ -28,201 +28,205 @@ info.break = () => console.log("─".repeat(88)) //Runner - try { - //Initialization - info.break() - info.section(`Metrics`) - - //Skip process if needed - if ((github.context.eventName === "push")&&(github.context.payload?.head_commit)) { - if (/\[Skip GitHub Action\]/.test(github.context.payload.head_commit.message)) { - console.log(`Skipped because [Skip GitHub Action] is in commit message`) - process.exit(0) - } - } - - //Load configuration - const {conf, Plugins, Templates} = await setup({log:false, nosettings:true, community:{templates:core.getInput("setup_community_templates")}}) - const {metadata} = conf - info("Setup", "complete") - info("Version", conf.package.version) - - //Core inputs - const { - user:_user, token, - template, query, "setup.community.templates":_templates, - filename, optimize, verify, - debug, "debug.flags":dflags, "use.mocked.data":mocked, dryrun, - "plugins.errors.fatal":die, - "committer.token":_token, "committer.branch":_branch, - "use.prebuilt.image":_image, - ...config - } = metadata.plugins.core.inputs.action({core}) - const q = {...query, template} - - //Docker image - if (_image) - info("Using prebuilt image", _image) - - //Debug mode and flags - info("Debug mode", debug) - if (!debug) { - console.debug = message => debugged.push(message) - DEBUG = false - } - info("Debug flags", dflags) - - //Token for data gathering - info("GitHub token", token, {token:true}) - if (!token) - throw new Error("You must provide a valid GitHub token to gather your metrics") - conf.settings.token = token - const api = {} - api.graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}}) - info("Github GraphQL API", "ok") - api.rest = github.getOctokit(token) - info("Github REST API", "ok") - //Apply mocking if needed - if (mocked) { - Object.assign(api, await mocks(api)) - info("Use mocked API", true) - } - //Extract octokits - const {graphql, rest} = api - - //GitHub user - let authenticated + ;(async function() { try { - authenticated = (await rest.users.getAuthenticated()).data.login - } - catch { - authenticated = github.context.repo.owner - } - const user = _user || authenticated - info("GitHub account", user) + //Initialization + info.break() + info.section("Metrics") - //Current repository - info("Current repository", `${github.context.repo.owner}/${github.context.repo.repo}`) - - //Committer - const committer = {} - if (!dryrun) { - //Compute committer informations - committer.commit = true - committer.token = _token || token - committer.branch = _branch || github.context.ref.replace(/^refs[/]heads[/]/, "") - info("Committer token", committer.token, {token:true}) - if (!committer.token) - throw new Error("You must provide a valid GitHub token to commit your metrics") - info("Committer branch", committer.branch) - //Instantiate API for committer - committer.rest = github.getOctokit(committer.token) - info("Committer REST API", "ok") - try { - info("Committer account", (await committer.rest.users.getAuthenticated()).data.login) - } - catch { - info("Committer account", "(github-actions)") - } - //Retrieve previous render SHA to be able to update file content through API - committer.sha = null - try { - const {repository:{object:{oid}}} = await graphql(` - query Sha { - repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") { - object(expression: "${committer.branch}:${filename}") { ... on Blob { oid } } - } - } - `, - {headers:{authorization:`token ${committer.token}`}} - ) - committer.sha = oid - } catch (error) { console.debug(error) } - info("Previous render sha", committer.sha ?? "(none)") - } - else - info("Dry-run", true) - - //SVG file - conf.optimize = optimize - info("SVG output", filename) - info("SVG optimization", optimize) - info("SVG verification after generation", verify) - - //Template - info.break() - info.section("Templates") - info("Community templates", _templates) - info("Template used", template) - info("Query additional params", query) - - //Core config - info.break() - info.group({metadata, name:"core", inputs:config}) - info("Plugin errors", die ? "(exit with error)" : "(displayed in generated SVG)") - Object.assign(q, config) - - //Base content - info.break() - const {base:parts, ...base} = metadata.plugins.base.inputs.action({core}) - info.group({metadata, name:"base", inputs:base}) - info("Base sections", parts) - base.base = false - for (const part of conf.settings.plugins.base.parts) - base[`base.${part}`] = parts.includes(part) - Object.assign(q, base) - - //Additional plugins - const plugins = {} - for (const name of Object.keys(Plugins).filter(key => !["base", "core"].includes(key))) { - //Parse inputs - const {[name]:enabled, ...inputs} = metadata.plugins[name].inputs.action({core}) - plugins[name] = {enabled} - //Register user inputs - if (enabled) { - info.break() - info.group({metadata, name, inputs}) - q[name] = true - for (const [key, value] of Object.entries(inputs)) { - //Store token in plugin configuration - if (metadata.plugins[name].inputs[key].type === "token") - plugins[name][key] = value - //Store value in query - else - q[`${name}.${key}`] = value + //Skip process if needed + if ((github.context.eventName === "push")&&(github.context.payload?.head_commit)) { + if (/\[Skip GitHub Action\]/.test(github.context.payload.head_commit.message)) { + console.log("Skipped because [Skip GitHub Action] is in commit message") + process.exit(0) } } + + //Load configuration + const {conf, Plugins, Templates} = await setup({log:false, nosettings:true, community:{templates:core.getInput("setup_community_templates")}}) + const {metadata} = conf + info("Setup", "complete") + info("Version", conf.package.version) + + //Core inputs + const { + user:_user, token, + template, query, "setup.community.templates":_templates, + filename, optimize, verify, + debug, "debug.flags":dflags, "use.mocked.data":mocked, dryrun, + "plugins.errors.fatal":die, + "committer.token":_token, "committer.branch":_branch, + "use.prebuilt.image":_image, + ...config + } = metadata.plugins.core.inputs.action({core}) + const q = {...query, template} + + //Docker image + if (_image) + info("Using prebuilt image", _image) + + //Debug mode and flags + info("Debug mode", debug) + if (!debug) { + console.debug = message => debugged.push(message) + DEBUG = false + } + info("Debug flags", dflags) + + //Token for data gathering + info("GitHub token", token, {token:true}) + if (!token) + throw new Error("You must provide a valid GitHub token to gather your metrics") + conf.settings.token = token + const api = {} + api.graphql = octokit.graphql.defaults({headers:{authorization:`token ${token}`}}) + info("Github GraphQL API", "ok") + api.rest = github.getOctokit(token) + info("Github REST API", "ok") + //Apply mocking if needed + if (mocked) { + Object.assign(api, await mocks(api)) + info("Use mocked API", true) + } + //Extract octokits + const {graphql, rest} = api + + //GitHub user + let authenticated + try { + authenticated = (await rest.users.getAuthenticated()).data.login + } + catch { + authenticated = github.context.repo.owner + } + const user = _user || authenticated + info("GitHub account", user) + + //Current repository + info("Current repository", `${github.context.repo.owner}/${github.context.repo.repo}`) + + //Committer + const committer = {} + if (!dryrun) { + //Compute committer informations + committer.commit = true + committer.token = _token || token + committer.branch = _branch || github.context.ref.replace(/^refs[/]heads[/]/, "") + info("Committer token", committer.token, {token:true}) + if (!committer.token) + throw new Error("You must provide a valid GitHub token to commit your metrics") + info("Committer branch", committer.branch) + //Instantiate API for committer + committer.rest = github.getOctokit(committer.token) + info("Committer REST API", "ok") + try { + info("Committer account", (await committer.rest.users.getAuthenticated()).data.login) + } + catch { + info("Committer account", "(github-actions)") + } + //Retrieve previous render SHA to be able to update file content through API + committer.sha = null + try { + const {repository:{object:{oid}}} = await graphql(` + query Sha { + repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") { + object(expression: "${committer.branch}:${filename}") { ... on Blob { oid } } + } + } + `, {headers:{authorization:`token ${committer.token}`}}) + committer.sha = oid + } + catch (error) { + console.debug(error) + } + info("Previous render sha", committer.sha ?? "(none)") + } + else + info("Dry-run", true) + + //SVG file + conf.optimize = optimize + info("SVG output", filename) + info("SVG optimization", optimize) + info("SVG verification after generation", verify) + + //Template + info.break() + info.section("Templates") + info("Community templates", _templates) + info("Template used", template) + info("Query additional params", query) + + //Core config + info.break() + info.group({metadata, name:"core", inputs:config}) + info("Plugin errors", die ? "(exit with error)" : "(displayed in generated SVG)") + Object.assign(q, config) + + //Base content + info.break() + const {base:parts, ...base} = metadata.plugins.base.inputs.action({core}) + info.group({metadata, name:"base", inputs:base}) + info("Base sections", parts) + base.base = false + for (const part of conf.settings.plugins.base.parts) + base[`base.${part}`] = parts.includes(part) + Object.assign(q, base) + + //Additional plugins + const plugins = {} + for (const name of Object.keys(Plugins).filter(key => !["base", "core"].includes(key))) { + //Parse inputs + const {[name]:enabled, ...inputs} = metadata.plugins[name].inputs.action({core}) + plugins[name] = {enabled} + //Register user inputs + if (enabled) { + info.break() + info.group({metadata, name, inputs}) + q[name] = true + for (const [key, value] of Object.entries(inputs)) { + //Store token in plugin configuration + if (metadata.plugins[name].inputs[key].type === "token") + plugins[name][key] = value + //Store value in query + else + q[`${name}.${key}`] = value + } + } + } + + //Render metrics + info.break() + info.section("Rendering") + const {rendered} = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die, verify}, {Plugins, Templates}) + info("Status", "complete") + + //Commit metrics + if (committer.commit) { + await committer.rest.repos.createOrUpdateFileContents({ + ...github.context.repo, path:filename, message:`Update ${filename} - [Skip GitHub Action]`, + content:Buffer.from(rendered).toString("base64"), + branch:committer.branch, + ...(committer.sha ? {sha:committer.sha} : {}), + }) + info("Commit to repository", "success") + } + + //Success + info.break() + console.log("Success, thanks for using metrics!") + process.exit(0) } - - //Render metrics - info.break() - info.section("Rendering") - const {rendered} = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die, verify}, {Plugins, Templates}) - info("Status", "complete") - - //Commit metrics - if (committer.commit) { - await committer.rest.repos.createOrUpdateFileContents({ - ...github.context.repo, path:filename, message:`Update ${filename} - [Skip GitHub Action]`, - content:Buffer.from(rendered).toString("base64"), - branch:committer.branch, - ...(committer.sha ? {sha:committer.sha} : {}) - }) - info("Commit to repository", "success") + //Errors + catch (error) { + console.error(error) + //Print debug buffer if debug was not enabled (if it is, it's already logged on the fly) + if (!DEBUG) { + for (const log of [info.break(), "An error occured, logging debug message :", ...debugged]) + console.log(log) + } + core.setFailed(error.message) + process.exit(1) } - - //Success - info.break() - console.log(`Success, thanks for using metrics!`) - process.exit(0) - } -//Errors - catch (error) { - console.error(error) - //Print debug buffer if debug was not enabled (if it is, it's already logged on the fly) - if (!DEBUG) - for (const log of [info.break(), "An error occured, logging debug message :", ...debugged]) - console.log(log) - core.setFailed(error.message) - process.exit(1) - } + })() \ No newline at end of file diff --git a/source/app/metrics/index.mjs b/source/app/metrics/index.mjs index 5b663e6f..3fdb0ec1 100644 --- a/source/app/metrics/index.mjs +++ b/source/app/metrics/index.mjs @@ -1,8 +1,8 @@ //Imports - import util from "util" - import ejs from "ejs" - import SVGO from "svgo" import * as utils from "./utils.mjs" + import ejs from "ejs" + import util from "util" + import SVGO from "svgo" //Setup export default async function metrics({login, q, dflags = []}, {graphql, rest, plugins, conf, die = false, verify = false, convert = null}, {Plugins, Templates}) { @@ -23,7 +23,7 @@ //Initialization const pending = [] - const queries = conf.queries + const {queries} = conf const data = {animated:true, base:{}, config:{}, errors:[], plugins:{}, computed:{}} const imports = {plugins:Plugins, templates:Templates, metadata:conf.metadata, ...utils} @@ -47,7 +47,7 @@ if (errors.length) { console.warn(`metrics/compute/${login} > ${errors.length} errors !`) if (die) - throw new Error(`An error occured during rendering, dying`) + throw new Error("An error occured during rendering, dying") else console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256})) } diff --git a/source/app/metrics/metadata.mjs b/source/app/metrics/metadata.mjs index 037248f0..681fc261 100644 --- a/source/app/metrics/metadata.mjs +++ b/source/app/metrics/metadata.mjs @@ -1,10 +1,10 @@ //Imports import fs from "fs" import path from "path" - import yaml from "js-yaml" import url from "url" + import yaml from "js-yaml" -/** Metadata descriptor parser */ +/**Metadata descriptor parser */ export default async function metadata({log = true} = {}) { //Paths const __metrics = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "../../..") @@ -16,7 +16,7 @@ //Load plugins metadata let Plugins = {} - logger(`metrics/metadata > loading plugins metadata`) + logger("metrics/metadata > loading plugins metadata") for (const name of await fs.promises.readdir(__plugins)) { if (!(await fs.promises.lstat(path.join(__plugins, name))).isDirectory()) continue @@ -29,7 +29,7 @@ //Load templates metadata let Templates = {} - logger(`metrics/metadata > loading templates metadata`) + logger("metrics/metadata > loading templates metadata") for (const name of await fs.promises.readdir(__templates)) { if (!(await fs.promises.lstat(path.join(__templates, name))).isDirectory()) continue @@ -46,8 +46,8 @@ return {plugins:Plugins, templates:Templates} } -/** Metadata extractor for templates */ - metadata.plugin = async function ({__plugins, name, logger}) { +/**Metadata extractor for templates */ + metadata.plugin = async function({__plugins, name, logger}) { try { //Load meta descriptor const raw = `${await fs.promises.readFile(path.join(__plugins, name, "metadata.yml"), "utf-8")}` @@ -55,7 +55,7 @@ //Inputs parser { - meta.inputs = function ({data:{user = null} = {}, q, account}, defaults = {}) { + meta.inputs = function({data:{user = null} = {}, q, account}, defaults = {}) { //Support check if (!account) logger(`metrics/inputs > account type not set for plugin ${name}!`) @@ -142,7 +142,7 @@ return value } } - })(defaults[key] ?? defaulted) + })(defaults[key] ?? defaulted), ])) logger(`metrics/inputs > ${name} > ${JSON.stringify(result)}`) return result @@ -154,7 +154,7 @@ { //Extract comments const comments = {} - raw.split(/(\r?\n){2,}/m) + raw.split(/(?:\r?\n){2,}/m) .map(x => x.trim()).filter(x => x) .map(x => x.split("\n").map(y => y.trim()).join("\n")) .map(x => { @@ -168,12 +168,12 @@ key, { comment:comments[key] ?? "", - descriptor:yaml.dump({[key]:Object.fromEntries(Object.entries(value).filter(([key]) => ["description", "default", "required"].includes(key)))}, {quotingType:'"', noCompatMode:true}) - } + descriptor:yaml.dump({[key]:Object.fromEntries(Object.entries(value).filter(([key]) => ["description", "default", "required"].includes(key)))}, {quotingType:'"', noCompatMode:true}), + }, ])) //Action inputs - meta.inputs.action = function ({core}) { + meta.inputs.action = function({core}) { //Build query object from inputs const q = {} for (const key of Object.keys(inputs)) { @@ -207,15 +207,14 @@ case "string":{ if (Array.isArray(values)) return {text, type:"select", values} - else - return {text, type:"text", placeholder:example ?? defaulted, defaulted} + return {text, type:"text", placeholder:example ?? defaulted, defaulted} } case "json": return {text, type:"text", placeholder:example ?? defaulted, defaulted} default: return null } - })() + })(), ]).filter(([key, value]) => (value)&&(key !== name))) } @@ -241,8 +240,8 @@ } } -/** Metadata extractor for templates */ - metadata.template = async function ({__templates, name, plugins, logger}) { +/**Metadata extractor for templates */ + metadata.template = async function({__templates, name, plugins, logger}) { try { //Load meta descriptor const raw = `${await fs.promises.readFile(path.join(__templates, name, "README.md"), "utf-8")}` @@ -262,7 +261,7 @@ return { name:raw.match(/^### (?[\s\S]+?)\n/)?.groups?.name?.trim(), readme:{ - demo:raw.match(/(?[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? (name === "community" ? `` : ""), + demo:raw.match(/(?
See documentation 🌍
[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? (name === "community" ? "" : ""), compatibility:{...compatibility, base:true}, }, } @@ -273,10 +272,10 @@ } } -/** Metadata converters */ +/**Metadata converters */ metadata.to = { query(key, {name = null} = {}) { key = key.replace(/^plugin_/, "").replace(/_/g, ".") return name ? key.replace(new RegExp(`^(${name}.)`, "g"), "") : key - } - } \ No newline at end of file + }, + } diff --git a/source/app/metrics/setup.mjs b/source/app/metrics/setup.mjs index b2cedd50..bcd401a4 100644 --- a/source/app/metrics/setup.mjs +++ b/source/app/metrics/setup.mjs @@ -1,17 +1,17 @@ //Imports import fs from "fs" + import metadata from "./metadata.mjs" import path from "path" + import processes from "child_process" import util from "util" import url from "url" - import processes from "child_process" - import metadata from "./metadata.mjs" //Templates and plugins const Templates = {} const Plugins = {} -/** Setup */ - export default async function ({log = true, nosettings = false, community = {}} = {}) { +/**Setup */ + export default async function({log = true, nosettings = false, community = {}} = {}) { //Paths const __metrics = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "../../..") @@ -24,7 +24,7 @@ //Init const logger = log ? console.debug : () => null - logger(`metrics/setup > setup`) + logger("metrics/setup > setup") const conf = { templates:{}, queries:{}, @@ -34,21 +34,21 @@ statics:__statics, templates:__templates, node_modules:__modules, - } + }, } //Load settings - logger(`metrics/setup > load settings.json`) + logger("metrics/setup > load settings.json") if (fs.existsSync(__settings)) { if (nosettings) - logger(`metrics/setup > load settings.json > skipped because no settings is enabled`) + logger("metrics/setup > load settings.json > skipped because no settings is enabled") else { conf.settings = JSON.parse(`${await fs.promises.readFile(__settings)}`) - logger(`metrics/setup > load settings.json > success`) + logger("metrics/setup > load settings.json > success") } } else - logger(`metrics/setup > load settings.json > (missing)`) + logger("metrics/setup > load settings.json > (missing)") if (!conf.settings.templates) conf.settings.templates = {default:"classic", enabled:[]} if (!conf.settings.plugins) @@ -59,13 +59,13 @@ logger(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256})) //Load package settings - logger(`metrics/setup > load package.json`) + logger("metrics/setup > load package.json") conf.package = JSON.parse(`${await fs.promises.readFile(__package)}`) - logger(`metrics/setup > load package.json > success`) + logger("metrics/setup > load package.json > success") //Load community templates if ((typeof conf.settings.community.templates === "string")&&(conf.settings.community.templates.length)) { - logger(`metrics/setup > parsing community templates list`) + logger("metrics/setup > parsing community templates list") conf.settings.community.templates = [...new Set([...decodeURIComponent(conf.settings.community.templates).split(",").map(v => v.trim().toLocaleLowerCase()).filter(v => v)])] } if ((Array.isArray(conf.settings.community.templates))&&(conf.settings.community.templates.length)) { @@ -77,7 +77,7 @@ try { //Parse community template logger(`metrics/setup > load community template ${template}`) - const {repo, branch, name, trust = false} = template.match(/^(?[\s\S]+?)@(?[\s\S]+?):(?[\s\S]+?)(?[+]trust)?$/)?.groups + const {repo, branch, name, trust = false} = template.match(/^(?[\s\S]+?)@(?[\s\S]+?):(?[\s\S]+?)(?[+]trust)?$/)?.groups ?? null const command = `git clone --single-branch --branch ${branch} https://github.com/${repo}.git ${path.join(__templates, ".community")}` logger(`metrics/setup > run ${command}`) //Clone remote repository @@ -99,14 +99,15 @@ logger(`metrics/setup > clean ${repo}@${branch}`) await fs.promises.rmdir(path.join(__templates, ".community"), {recursive:true}) logger(`metrics/setup > loaded community template ${name}`) - } catch (error) { + } + catch (error) { logger(`metrics/setup > failed to load community template ${template}`) logger(error) } } } else - logger(`metrics/setup > no community templates to install`) + logger("metrics/setup > no community templates to install") //Load templates for (const name of await fs.promises.readdir(__templates)) { @@ -122,7 +123,7 @@ conf.templates[name] = {image, style, fonts, partials, views:[directory]} //Cache templates scripts - Templates[name] = await (async () => { + Templates[name] = await (async() => { const template = path.join(directory, "template.mjs") const fallback = path.join(__templates, "classic", "template.mjs") return (await import(url.pathToFileURL(fs.existsSync(template) ? template : fallback).href)).default @@ -137,7 +138,7 @@ const partials = JSON.parse(`${fs.readFileSync(path.join(directory, "partials/_.json"))}`) logger(`metrics/setup > reload template [${name}] > success`) return {image, style, fonts, partials, views:[directory]} - } + }, }) } } @@ -156,11 +157,12 @@ const __queries = path.join(directory, "queries") if (fs.existsSync(__queries)) { //Alias for default query - const queries = conf.queries[name] = function () { + const queries = function() { if (!queries[name]) throw new ReferenceError(`Default query for ${name} undefined`) return queries[name](...arguments) } + conf.queries[name] = queries //Load queries for (const file of await fs.promises.readdir(__queries)) { //Cache queries @@ -176,7 +178,7 @@ const raw = `${fs.readFileSync(path.join(__queries, file))}` logger(`metrics/setup > reload query [${name}/${query}] > success`) return raw - } + }, }) } } @@ -194,10 +196,12 @@ conf.metadata = await metadata({log}) //Set no token property - Object.defineProperty(conf.settings, "notoken", {get() { return conf.settings.token === "NOT_NEEDED" }}) + Object.defineProperty(conf.settings, "notoken", {get() { + return conf.settings.token === "NOT_NEEDED" +}}) //Conf - logger(`metrics/setup > setup > success`) + logger("metrics/setup > setup > success") return {Templates, Plugins, conf} - } \ No newline at end of file + } diff --git a/source/app/metrics/utils.mjs b/source/app/metrics/utils.mjs index 78cea488..ae680a08 100644 --- a/source/app/metrics/utils.mjs +++ b/source/app/metrics/utils.mjs @@ -8,48 +8,50 @@ import axios from "axios" import puppeteer from "puppeteer" import imgb64 from "image-to-base64" - import dayjs from 'dayjs'; - import utc from 'dayjs/plugin/utc.js'; - dayjs.extend(utc); + import dayjs from "dayjs" + import utc from "dayjs/plugin/utc.js" + dayjs.extend(utc) - export {fs, os, paths, url, util, processes, axios, puppeteer, imgb64, dayjs}; + export {fs, os, paths, url, util, processes, axios, puppeteer, imgb64, dayjs} -/** Returns module __dirname */ +/**Returns module __dirname */ export function __module(module) { return paths.join(paths.dirname(url.fileURLToPath(module))) } -/** Plural formatter */ +/**Plural formatter */ export function s(value, end = "") { return value !== 1 ? {y:"ies", "":"s"}[end] : end } -/** Formatter */ +/**Formatter */ export function format(n, {sign = false} = {}) { - for (const {u, v} of [{u:"b", v:10**9}, {u:"m", v:10**6}, {u:"k", v:10**3}]) + for (const {u, v} of [{u:"b", v:10**9}, {u:"m", v:10**6}, {u:"k", v:10**3}]) { if (n/v >= 1) return `${(sign)&&(n > 0) ? "+" : ""}${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")}${u}` + } return `${(sign)&&(n > 0) ? "+" : ""}${n}` } -/** Bytes formatter */ +/**Bytes formatter */ export function bytes(n) { - for (const {u, v} of [{u:"E", v:10**18}, {u:"P", v:10**15}, {u:"T", v:10**12}, {u:"G", v:10**9}, {u:"M", v:10**6}, {u:"k", v:10**3}]) + for (const {u, v} of [{u:"E", v:10**18}, {u:"P", v:10**15}, {u:"T", v:10**12}, {u:"G", v:10**9}, {u:"M", v:10**6}, {u:"k", v:10**3}]) { if (n/v >= 1) return `${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B` + } return `${n} byte${n > 1 ? "s" : ""}` } format.bytes = bytes -/** Percentage formatter */ +/**Percentage formatter */ export function percentage(n, {rescale = true} = {}) { return `${(n*(rescale ? 100 : 1)).toFixed(2) - .replace(/(?<=[.])([1-9]*)(0+)$/, (m, a, b) => a) + .replace(/(?<=[.])(?[1-9]*)0+$/, "$") .replace(/[.]$/, "")}%` } format.percentage = percentage -/** Text ellipsis formatter */ +/**Text ellipsis formatter */ export function ellipsis(text, {length = 20} = {}) { text = `${text}` if (text.length < length) @@ -58,7 +60,7 @@ } format.ellipsis = ellipsis -/** Array shuffler */ +/**Array shuffler */ export function shuffle(array) { for (let i = array.length-1; i > 0; i--) { const j = Math.floor(Math.random()*(i+1)) @@ -67,7 +69,7 @@ return array } -/** Escape html */ +/**Escape html */ export function htmlescape(string, u = {"&":true, "<":true, ">":true, '"':true, "'":true}) { return string .replace(/&(?!(?:amp|lt|gt|quot|apos);)/g, u["&"] ? "&" : "&") @@ -77,18 +79,19 @@ .replace(/'/g, u["'"] ? "'" : "'") } -/** Expand url */ +/**Expand url */ export async function urlexpand(url) { try { return (await axios.get(url)).request.res.responseUrl - } catch { + } + catch { return url } } -/** Run command */ +/**Run command */ export async function run(command, options) { - return await new Promise((solve, reject) => { + return new Promise((solve, reject) => { console.debug(`metrics/command > ${command}`) const child = processes.exec(command, options) let [stdout, stderr] = ["", ""] @@ -101,7 +104,7 @@ }) } -/** Render svg */ +/**Render svg */ export async function svgresize(svg, {paddings = ["6%"], convert} = {}) { //Instantiate browser if needed if (!svgresize.browser) { @@ -144,7 +147,7 @@ return {resized, mime} } -/** Wait */ +/**Wait */ export async function wait(seconds) { - await new Promise(solve => setTimeout(solve, seconds*1000)) - } \ No newline at end of file + await new Promise(solve => setTimeout(solve, seconds*1000)) //eslint-disable-line no-promise-executor-return + } diff --git a/source/app/mocks/.eslintrc.yml b/source/app/mocks/.eslintrc.yml new file mode 100644 index 00000000..24f4cc11 --- /dev/null +++ b/source/app/mocks/.eslintrc.yml @@ -0,0 +1,5 @@ +# Overrides enforced rules for mocks +rules: + max-params: off + no-unused-vars: off + prefer-named-capture-group: off \ No newline at end of file diff --git a/source/app/mocks/api/axios/get/lastfm.mjs b/source/app/mocks/api/axios/get/lastfm.mjs index aceea2e9..da554197 100644 --- a/source/app/mocks/api/axios/get/lastfm.mjs +++ b/source/app/mocks/api/axios/get/lastfm.mjs @@ -1,5 +1,5 @@ -/** Mocked data */ - export default function ({faker, url, options, login = faker.internet.userName()}) { +/**Mocked data */ + export default function({faker, url, options, login = faker.internet.userName()}) { //Last.fm api if (/^https:..ws.audioscrobbler.com.*$/.test(url)) { //Get recently played tracks @@ -63,4 +63,4 @@ }) } } - } \ No newline at end of file + } diff --git a/source/app/mocks/api/axios/get/pagespeed.mjs b/source/app/mocks/api/axios/get/pagespeed.mjs index 0e23f99d..77ccc93b 100644 --- a/source/app/mocks/api/axios/get/pagespeed.mjs +++ b/source/app/mocks/api/axios/get/pagespeed.mjs @@ -1,5 +1,5 @@ -/** Mocked data */ - export default function ({faker, url, options, login = faker.internet.userName()}) { +/**Mocked data */ + export default function({faker, url, options, login = faker.internet.userName()}) { //Tested url const tested = url.match(/&url=(?.*?)(?:&|$)/)?.groups?.tested ?? faker.internet.url() //Pagespeed api @@ -20,17 +20,17 @@ "final-screenshot":{ id:"final-screenshot", title:"Final Screenshot", - score: null, + score:null, details:{ data:"data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", type:"screenshot", - timestamp:Date.now() - } + timestamp:Date.now(), + }, }, metrics:{ id:"metrics", title:"Metrics", - score: null, + score:null, details:{ items:[ { @@ -68,9 +68,9 @@ interactive:faker.random.number(1000), observedNavigationStartTs:faker.time.recent(), observedNavigationStart:faker.random.number(10), - observedFirstMeaningfulPaintTs:faker.time.recent() + observedFirstMeaningfulPaintTs:faker.time.recent(), }, - ] + ], }, }, }, @@ -90,16 +90,16 @@ title:"Accessibility", score:faker.random.float({max:1}), }, - performance: { + performance:{ id:"performance", title:"Performance", score:faker.random.float({max:1}), - } + }, }, }, analysisUTCTimestamp:`${faker.date.recent()}`, - } + }, }) } } - } \ No newline at end of file + } diff --git a/source/app/mocks/api/axios/get/spotify.mjs b/source/app/mocks/api/axios/get/spotify.mjs index 4aa65faa..58b2535a 100644 --- a/source/app/mocks/api/axios/get/spotify.mjs +++ b/source/app/mocks/api/axios/get/spotify.mjs @@ -1,5 +1,5 @@ -/** Mocked data */ - export default function ({faker, url, options, login = faker.internet.userName()}) { +/**Mocked data */ + export default function({faker, url, options, login = faker.internet.userName()}) { //Spotify api if (/^https:..api.spotify.com.*$/.test(url)) { //Get recently played tracks @@ -19,24 +19,24 @@ { name:artist, type:"artist", - } + }, ], images:[ { height:640, url:faker.image.abstract(), - width:640 + width:640, }, { height:300, url:faker.image.abstract(), - width:300 + width:300, }, { height:64, url:faker.image.abstract(), - width:64 - } + width:64, + }, ], name:track, release_date:`${faker.date.past()}`.substring(0, 10), @@ -46,7 +46,7 @@ { name:artist, type:"artist", - } + }, ], name:track, preview_url:faker.internet.url(), @@ -55,11 +55,11 @@ played_at:`${faker.date.recent()}`, context:{ type:"album", - } + }, }, ], - } + }, }) } } - } \ No newline at end of file + } diff --git a/source/app/mocks/api/axios/get/twitter.mjs b/source/app/mocks/api/axios/get/twitter.mjs index d9b37dcc..ab7281ed 100644 --- a/source/app/mocks/api/axios/get/twitter.mjs +++ b/source/app/mocks/api/axios/get/twitter.mjs @@ -1,5 +1,5 @@ -/** Mocked data */ - export default function ({faker, url, options, login = faker.internet.userName()}) { +/**Mocked data */ + export default function({faker, url, options, login = faker.internet.userName()}) { //Twitter api if (/^https:..api.twitter.com.*$/.test(url)) { //Get user profile @@ -16,7 +16,7 @@ id:faker.random.number(1000000).toString(), username, }, - } + }, }) } //Get recent tweets @@ -40,7 +40,7 @@ id:faker.random.number(100000000000000).toString(), created_at:`${faker.date.recent()}`, text:faker.lorem.paragraph(), - } + }, ], includes:{ users:[ @@ -49,7 +49,7 @@ name:"lowlighter", username:"lowlighter", }, - ] + ], }, meta:{ newest_id:faker.random.number(100000000000000).toString(), @@ -57,8 +57,8 @@ result_count:2, next_token:"MOCKED_CURSOR", }, - } + }, }) } } - } \ No newline at end of file + } diff --git a/source/app/mocks/api/axios/get/wakatime.mjs b/source/app/mocks/api/axios/get/wakatime.mjs index a256582b..9214c7bd 100644 --- a/source/app/mocks/api/axios/get/wakatime.mjs +++ b/source/app/mocks/api/axios/get/wakatime.mjs @@ -1,14 +1,16 @@ -/** Mocked data */ - export default function ({faker, url, options, login = faker.internet.userName()}) { +/**Mocked data */ + export default function({faker, url, options, login = faker.internet.userName()}) { //Wakatime api if (/^https:..wakatime.com.api.v1.users.current.stats.*$/.test(url)) { //Get user profile if (/api_key=MOCKED_TOKEN/.test(url)) { console.debug(`metrics/compute/mocks > mocking wakatime api result > ${url}`) - const stats = (array) => { + const stats = array => { const elements = [] let results = new Array(4+faker.random.number(2)).fill(null).map(_ => ({ - get digital() { return `${this.hours}:${this.minutes}` }, + get digital() { + return `${this.hours}:${this.minutes}` +}, hours:faker.random.number(1000), minutes:faker.random.number(1000), name:array ? faker.random.arrayElement(array) : faker.random.words(2).replace(/ /g, "-").toLocaleLowerCase(), percent:0, total_seconds:faker.random.number(1000000), @@ -43,8 +45,8 @@ total_seconds:faker.random.number(1000000000), total_seconds_including_other_language:faker.random.number(1000000000), }, - } + }, }) } } - } \ No newline at end of file + } diff --git a/source/app/mocks/api/axios/post/anilist.mjs b/source/app/mocks/api/axios/post/anilist.mjs index 22123bdb..4f86e236 100644 --- a/source/app/mocks/api/axios/post/anilist.mjs +++ b/source/app/mocks/api/axios/post/anilist.mjs @@ -1,8 +1,8 @@ -/** Mocked data */ - export default function ({faker, url, body, login = faker.internet.userName()}) { +/**Mocked data */ + export default function({faker, url, body, login = faker.internet.userName()}) { if (/^https:..graphql.anilist.co.*$/.test(url)) { //Initialization and media generator - const query = body.query + const {query} = body const media = ({type}) => ({ title:{romaji:faker.lorem.words(), english:faker.lorem.words(), native:faker.lorem.words()}, description:faker.lorem.paragraphs(), @@ -15,11 +15,11 @@ countryOfOrigin:"JP", genres:new Array(6).fill(null).map(_ => faker.lorem.word()), coverImage:{medium:null}, - startDate:{year:faker.date.past(20).getFullYear()} + startDate:{year:faker.date.past(20).getFullYear()}, }) //User statistics query if (/^query Statistics /.test(query)) { - console.debug(`metrics/compute/mocks > mocking anilist api result > Statistics`) + console.debug("metrics/compute/mocks > mocking anilist api result > Statistics") return ({ status:200, data:{ @@ -41,15 +41,15 @@ volumesRead:faker.random.number(10000), genres:new Array(4).fill(null).map(_ => ({genre:faker.lorem.word()})), }, - } - } - } - } + }, + }, + }, + }, }) } //Favorites characters if (/^query FavoritesCharacters /.test(query)) { - console.debug(`metrics/compute/mocks > mocking anilist api result > Favorites characters`) + console.debug("metrics/compute/mocks > mocking anilist api result > Favorites characters") return ({ status:200, data:{ @@ -59,20 +59,19 @@ characters:{ nodes:new Array(2+faker.random.number(16)).fill(null).map(_ => ({ name:{full:faker.name.findName(), native:faker.name.findName()}, - image:{medium:null} - }), - ), - pageInfo:{currentPage:1, hasNextPage:false} - } - } - } - } - } + image:{medium:null}, + })), + pageInfo:{currentPage:1, hasNextPage:false}, + }, + }, + }, + }, + }, }) } //Favorites anime/manga query if (/^query Favorites /.test(query)) { - console.debug(`metrics/compute/mocks > mocking anilist api result > Favorites`) + console.debug("metrics/compute/mocks > mocking anilist api result > Favorites") const type = /anime[(]/.test(query) ? "ANIME" : /manga[(]/.test(query) ? "MANGA" : "OTHER" return ({ status:200, @@ -83,17 +82,17 @@ [type.toLocaleLowerCase()]:{ nodes:new Array(16).fill(null).map(_ => media({type})), pageInfo:{currentPage:1, hasNextPage:false}, - } - } - } - } - } + }, + }, + }, + }, + }, }) } //Medias query if (/^query Medias /.test(query)) { - console.debug(`metrics/compute/mocks > mocking anilist api result > Medias`) - const type = body.variables.type + console.debug("metrics/compute/mocks > mocking anilist api result > Medias") + const {type} = body.variables return ({ status:200, data:{ @@ -106,19 +105,19 @@ entries:new Array(16).fill(null).map(_ => ({ status:faker.random.arrayElement(["CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"]), progress:faker.random.number(100), - progressVolumes: null, + progressVolumes:null, score:0, startedAt:{year:null, month:null, day:null}, completedAt:{year:null, month:null, day:null}, - media:media({type}) + media:media({type}), })), - } - ] - } - } - } + }, + ], + }, + }, + }, }) } } - } \ No newline at end of file + } diff --git a/source/app/mocks/api/axios/post/spotify.mjs b/source/app/mocks/api/axios/post/spotify.mjs index 2c92b2e4..c532b895 100644 --- a/source/app/mocks/api/axios/post/spotify.mjs +++ b/source/app/mocks/api/axios/post/spotify.mjs @@ -1,8 +1,8 @@ //Imports import urls from "url" -/** Mocked data */ - export default function ({faker, url, body, login = faker.internet.userName()}) { +/**Mocked data */ + export default function({faker, url, body, login = faker.internet.userName()}) { if (/^https:..accounts.spotify.com.api.token.*$/.test(url)) { //Access token generator const params = new urls.URLSearchParams(body) @@ -15,8 +15,8 @@ token_type:"Bearer", expires_in:3600, scope:"user-read-recently-played user-read-private", - } + }, }) } } - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/graphql/base.repositories.mjs b/source/app/mocks/api/github/graphql/base.repositories.mjs index 55f8b5fa..998b48cf 100644 --- a/source/app/mocks/api/github/graphql/base.repositories.mjs +++ b/source/app/mocks/api/github/graphql/base.repositories.mjs @@ -1,19 +1,19 @@ -/** Mocked data */ - export default function ({faker, query, login = faker.internet.userName()}) { - console.debug(`metrics/compute/mocks > mocking graphql api result > base/repositories`) +/**Mocked data */ + export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > base/repositories") return /after: "MOCKED_CURSOR"/m.test(query) ? ({ user:{ repositories:{ edges:[], nodes:[], - } - } + }, + }, }) : ({ user:{ repositories:{ edges:[ { - cursor:"MOCKED_CURSOR" + cursor:"MOCKED_CURSOR", }, ], nodes:[ @@ -31,7 +31,7 @@ {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, - ] + ], }, issues_open:{totalCount:faker.random.number(100)}, issues_closed:{totalCount:faker.random.number(100)}, @@ -39,10 +39,10 @@ pr_merged:{totalCount:faker.random.number(100)}, releases:{totalCount:faker.random.number(100)}, forkCount:faker.random.number(100), - licenseInfo:{spdxId:"MIT"} + licenseInfo:{spdxId:"MIT"}, }, - ] - } - } + ], + }, + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/graphql/base.repository.mjs b/source/app/mocks/api/github/graphql/base.repository.mjs index e5dcd373..421a727e 100644 --- a/source/app/mocks/api/github/graphql/base.repository.mjs +++ b/source/app/mocks/api/github/graphql/base.repository.mjs @@ -1,6 +1,6 @@ -/** Mocked data */ - export default function ({faker, query, login = faker.internet.userName()}) { - console.debug(`metrics/compute/mocks > mocking graphql api result > base/repository`) +/**Mocked data */ + export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > base/repository") return ({ user:{ repository:{ @@ -20,7 +20,7 @@ {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, - ] + ], }, issues_open:{totalCount:faker.random.number(100)}, issues_closed:{totalCount:faker.random.number(100)}, @@ -28,8 +28,8 @@ pr_merged:{totalCount:faker.random.number(100)}, releases:{totalCount:faker.random.number(100)}, forkCount:faker.random.number(100), - licenseInfo:{spdxId:"MIT"} + licenseInfo:{spdxId:"MIT"}, }, - } + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/graphql/base.user.mjs b/source/app/mocks/api/github/graphql/base.user.mjs index fdd40781..f452f0bc 100644 --- a/source/app/mocks/api/github/graphql/base.user.mjs +++ b/source/app/mocks/api/github/graphql/base.user.mjs @@ -1,8 +1,8 @@ -/** Mocked data */ - export default function ({faker, query, login = faker.internet.userName()}) { - console.debug(`metrics/compute/mocks > mocking graphql api result > base/user`) +/**Mocked data */ + export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > base/user") return ({ - user: { + user:{ databaseId:faker.random.number(10000000), name:faker.name.findName(), login, @@ -35,7 +35,7 @@ {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, - ] + ], }, { contributionDays:[ @@ -46,23 +46,23 @@ {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, - ] + ], }, { contributionDays:[ {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, - ] - } - ] - } + ], + }, + ], + }, }, repositoriesContributedTo:{totalCount:faker.random.number(100)}, followers:{totalCount:faker.random.number(1000)}, following:{totalCount:faker.random.number(1000)}, issueComments:{totalCount:faker.random.number(1000)}, - organizations:{totalCount:faker.random.number(10)} - } + organizations:{totalCount:faker.random.number(10)}, + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/graphql/gists.default.mjs b/source/app/mocks/api/github/graphql/gists.default.mjs index 11356269..e30abe2a 100644 --- a/source/app/mocks/api/github/graphql/gists.default.mjs +++ b/source/app/mocks/api/github/graphql/gists.default.mjs @@ -1,19 +1,19 @@ -/** Mocked data */ - export default function ({faker, query, login = faker.internet.userName()}) { - console.debug(`metrics/compute/mocks > mocking graphql api result > gists/default`) +/**Mocked data */ + export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > gists/default") return /after: "MOCKED_CURSOR"/m.test(query) ? ({ user:{ gists:{ edges:[], nodes:[], - } - } + }, + }, }) : ({ user:{ gists:{ edges:[ { - cursor:"MOCKED_CURSOR" + cursor:"MOCKED_CURSOR", }, ], totalCount:faker.random.number(100), @@ -23,17 +23,17 @@ isFork:false, forks:{totalCount:faker.random.number(10)}, files:[{name:faker.system.fileName()}], - comments:{totalCount:faker.random.number(10)} + comments:{totalCount:faker.random.number(10)}, }, { stargazerCount:faker.random.number(10), isFork:false, forks:{totalCount:faker.random.number(10)}, files:[{name:faker.system.fileName()}], - comments:{totalCount:faker.random.number(10)} - } - ] - } - } + comments:{totalCount:faker.random.number(10)}, + }, + ], + }, + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/graphql/isocalendar.calendar.mjs b/source/app/mocks/api/github/graphql/isocalendar.calendar.mjs index e1d6b8c3..cbe59235 100644 --- a/source/app/mocks/api/github/graphql/isocalendar.calendar.mjs +++ b/source/app/mocks/api/github/graphql/isocalendar.calendar.mjs @@ -1,6 +1,6 @@ -/** Mocked data */ - export default function ({faker, query, login = faker.internet.userName()}) { - console.debug(`metrics/compute/mocks > mocking graphql api result > isocalendar/calendar`) +/**Mocked data */ + export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > isocalendar/calendar") //Generate calendar const date = new Date(query.match(/from: "(?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"/)?.groups?.date) const to = new Date(query.match(/to: "(?\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"/)?.groups?.date) @@ -17,16 +17,16 @@ contributionDays.push({ contributionCount, color:["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"][Math.ceil(contributionCount/10/0.25)], - date:date.toISOString().substring(0, 10) + date:date.toISOString().substring(0, 10), }) } return ({ - user: { + user:{ calendar:{ contributionCalendar:{ - weeks - } - } - } + weeks, + }, + }, + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/graphql/people.default.mjs b/source/app/mocks/api/github/graphql/people.default.mjs index 4d6e2312..1ea846a2 100644 --- a/source/app/mocks/api/github/graphql/people.default.mjs +++ b/source/app/mocks/api/github/graphql/people.default.mjs @@ -1,13 +1,13 @@ -/** Mocked data */ - export default function ({faker, query, login = faker.internet.userName()}) { - console.debug(`metrics/compute/mocks > mocking graphql api result > people/default`) +/**Mocked data */ + export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > people/default") const type = query.match(/(?followers|following)[(]/)?.groups?.type ?? "(unknown type)" return /after: "MOCKED_CURSOR"/m.test(query) ? ({ user:{ [type]:{ edges:[], - } - } + }, + }, }) : ({ user:{ [type]:{ @@ -16,9 +16,9 @@ node:{ login, avatarUrl:null, - } - })) - } - } + }, + })), + }, + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/graphql/people.repository.mjs b/source/app/mocks/api/github/graphql/people.repository.mjs index 670b9f6a..067613ad 100644 --- a/source/app/mocks/api/github/graphql/people.repository.mjs +++ b/source/app/mocks/api/github/graphql/people.repository.mjs @@ -1,15 +1,15 @@ -/** Mocked data */ - export default function ({faker, query, login = faker.internet.userName()}) { - console.debug(`metrics/compute/mocks > mocking graphql api result > People`) +/**Mocked data */ + export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > People") const type = query.match(/(?stargazers|watchers)[(]/)?.groups?.type ?? "(unknown type)" return /after: "MOCKED_CURSOR"/m.test(query) ? ({ user:{ repository:{ [type]:{ edges:[], - } - } - } + }, + }, + }, }) : ({ user:{ repository:{ @@ -19,10 +19,10 @@ node:{ login, avatarUrl:null, - } - })) - } - } - } + }, + })), + }, + }, + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/graphql/people.sponsors.mjs b/source/app/mocks/api/github/graphql/people.sponsors.mjs index 353e6e11..82d4437f 100644 --- a/source/app/mocks/api/github/graphql/people.sponsors.mjs +++ b/source/app/mocks/api/github/graphql/people.sponsors.mjs @@ -1,14 +1,14 @@ -/** Mocked data */ - export default function ({faker, query, login = faker.internet.userName()}) { - console.debug(`metrics/compute/mocks > mocking graphql api result > People`) +/**Mocked data */ + export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > People") const type = query.match(/(?sponsorshipsAsSponsor|sponsorshipsAsMaintainer)[(]/)?.groups?.type ?? "(unknown type)" return /after: "MOCKED_CURSOR"/m.test(query) ? ({ user:{ login, [type]:{ - edges:[] - } - } + edges:[], + }, + }, }) : ({ user:{ login, @@ -23,10 +23,10 @@ sponsorable:{ login:faker.internet.userName(), avatarUrl:null, - } - } - })) - } - } + }, + }, + })), + }, + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/graphql/projects.repository.mjs b/source/app/mocks/api/github/graphql/projects.repository.mjs index 179f846a..21adbef4 100644 --- a/source/app/mocks/api/github/graphql/projects.repository.mjs +++ b/source/app/mocks/api/github/graphql/projects.repository.mjs @@ -1,6 +1,6 @@ -/** Mocked data */ - export default function ({faker, query, login = faker.internet.userName()}) { - console.debug(`metrics/compute/mocks > mocking graphql api result > projects/repository`) +/**Mocked data */ + export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > projects/repository") return ({ user:{ repository:{ @@ -12,10 +12,10 @@ doneCount:faker.random.number(10), inProgressCount:faker.random.number(10), todoCount:faker.random.number(10), - enabled:true - } - } - } - } + enabled:true, + }, + }, + }, + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/graphql/projects.user.mjs b/source/app/mocks/api/github/graphql/projects.user.mjs index 04c06f29..046673e0 100644 --- a/source/app/mocks/api/github/graphql/projects.user.mjs +++ b/source/app/mocks/api/github/graphql/projects.user.mjs @@ -1,6 +1,6 @@ -/** Mocked data */ - export default function ({faker, query, login = faker.internet.userName()}) { - console.debug(`metrics/compute/mocks > mocking graphql api result > projects/user`) +/**Mocked data */ + export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > projects/user") return ({ user:{ projects:{ @@ -14,11 +14,11 @@ doneCount:faker.random.number(10), inProgressCount:faker.random.number(10), todoCount:faker.random.number(10), - enabled:true - } - } - ] - } - } + enabled:true, + }, + }, + ], + }, + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/graphql/stargazers.default.mjs b/source/app/mocks/api/github/graphql/stargazers.default.mjs index 2bae7cb9..88e63048 100644 --- a/source/app/mocks/api/github/graphql/stargazers.default.mjs +++ b/source/app/mocks/api/github/graphql/stargazers.default.mjs @@ -1,20 +1,20 @@ -/** Mocked data */ - export default function ({faker, query, login = faker.internet.userName()}) { - console.debug(`metrics/compute/mocks > mocking graphql api result > stargazers/default`) +/**Mocked data */ + export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > stargazers/default") return /after: "MOCKED_CURSOR"/m.test(query) ? ({ repository:{ stargazers:{ edges:[], - } - } + }, + }, }) : ({ repository:{ stargazers:{ edges:new Array(faker.random.number({min:50, max:100})).fill(null).map(() => ({ starredAt:`${faker.date.recent(30)}`, - cursor:"MOCKED_CURSOR" - })) - } - } + cursor:"MOCKED_CURSOR", + })), + }, + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/graphql/stars.default.mjs b/source/app/mocks/api/github/graphql/stars.default.mjs index 5e85daa8..d8154410 100644 --- a/source/app/mocks/api/github/graphql/stars.default.mjs +++ b/source/app/mocks/api/github/graphql/stars.default.mjs @@ -1,6 +1,6 @@ -/** Mocked data */ - export default function ({faker, query, login = faker.internet.userName()}) { - console.debug(`metrics/compute/mocks > mocking graphql api result > stars/default`) +/**Mocked data */ + export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > stars/default") return ({ user:{ starredRepositories:{ @@ -22,16 +22,16 @@ stargazerCount:faker.random.number(10000), licenseInfo:{ nickname:null, - name:"MIT License" + name:"MIT License", }, primaryLanguage:{ color:"#f1e05a", - name:"JavaScript" - } - } + name:"JavaScript", + }, + }, }, - ] - } - } + ], + }, + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/rest/activity/listEventsForAuthenticatedUser.mjs b/source/app/mocks/api/github/rest/activity/listEventsForAuthenticatedUser.mjs index aa036b15..65efd5d1 100644 --- a/source/app/mocks/api/github/rest/activity/listEventsForAuthenticatedUser.mjs +++ b/source/app/mocks/api/github/rest/activity/listEventsForAuthenticatedUser.mjs @@ -1,6 +1,6 @@ -/** Mocked data */ - export default function ({faker}, target, that, [{username:login, page, per_page}]) { - console.debug(`metrics/compute/mocks > mocking rest api result > rest.activity.listEventsForAuthenticatedUser`) +/**Mocked data */ + export default function({faker}, target, that, [{username:login, page, per_page}]) { + console.debug("metrics/compute/mocks > mocking rest api result > rest.activity.listEventsForAuthenticatedUser") return ({ status:200, url:`https://api.github.com/users/${login}/events?per_page=${per_page}&page=${page}`, @@ -27,7 +27,7 @@ path:faker.system.fileName(), commit_id:"MOCKED_SHA", body:faker.lorem.sentence(), - } + }, }, created_at:faker.date.recent(7), }, @@ -55,7 +55,7 @@ login:faker.internet.userName(), }, body:"", - } + }, }, created_at:faker.date.recent(7), }, @@ -77,8 +77,8 @@ login, }, body:faker.lorem.paragraph(), - performed_via_github_app:null - } + performed_via_github_app:null, + }, }, created_at:faker.date.recent(7), }, @@ -99,8 +99,8 @@ summary:null, action:"created", sha:"MOCKED_SHA", - } - ] + }, + ], }, created_at:faker.date.recent(7), }, @@ -125,14 +125,14 @@ { name:"lorem ipsum", color:"d876e3", - } + }, ], state:"open", }, comment:{ body:faker.lorem.paragraph(), - performed_via_github_app:null - } + performed_via_github_app:null, + }, }, created_at:faker.date.recent(7), }, @@ -149,7 +149,7 @@ forkee:{ name:faker.random.word(), full_name:`${faker.random.word()}/${faker.random.word()}`, - } + }, }, created_at:faker.date.recent(7), }, @@ -178,7 +178,7 @@ user:{ login:faker.internet.userName(), }, - } + }, }, created_at:faker.date.recent(7), }, @@ -198,7 +198,7 @@ name:faker.random.words(4), draft:faker.random.boolean(), prerelease:faker.random.boolean(), - } + }, }, created_at:faker.date.recent(7), }, @@ -262,8 +262,8 @@ sha:"MOCKED_SHA", message:faker.lorem.sentence(), url:"https://api.github.com/repos/lowlighter/metrics/commits/MOCKED_SHA", - } - ] + }, + ], }, created_at:faker.date.recent(7), }, @@ -288,7 +288,7 @@ additions:faker.random.number(1000), deletions:faker.random.number(1000), changed_files:faker.random.number(10), - } + }, }, created_at:faker.date.recent(7), }, @@ -305,7 +305,7 @@ member:{ login:faker.internet.userName(), }, - action:"added" + action:"added", }, created_at:faker.date.recent(7), }, @@ -320,7 +320,7 @@ }, payload:{}, created_at:faker.date.recent(7), - } - ] + }, + ], }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/rest/rateLimit/get.mjs b/source/app/mocks/api/github/rest/rateLimit/get.mjs index cd419cee..1364ab88 100644 --- a/source/app/mocks/api/github/rest/rateLimit/get.mjs +++ b/source/app/mocks/api/github/rest/rateLimit/get.mjs @@ -1,4 +1,4 @@ -/** Mocked data */ +/**Mocked data */ export default function({faker}, target, that, args) { return ({ status:200, @@ -17,7 +17,7 @@ source_import:{limit:100, used:0, remaining:100, reset:0}, code_scanning_upload:{limit:500, used:0, remaining:500, reset:0}, }, - rate:{limit:5000, used:0, remaining:"MOCKED", reset:0} - } + rate:{limit:5000, used:0, remaining:"MOCKED", reset:0}, + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/rest/repos/getContributorsStats.mjs b/source/app/mocks/api/github/rest/repos/getContributorsStats.mjs index e2fdaddd..9b046820 100644 --- a/source/app/mocks/api/github/rest/repos/getContributorsStats.mjs +++ b/source/app/mocks/api/github/rest/repos/getContributorsStats.mjs @@ -1,6 +1,6 @@ -/** Mocked data */ +/**Mocked data */ export default function({faker}, target, that, [{owner, repo}]) { - console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.getContributorsStats`) + console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getContributorsStats") return ({ status:200, url:`https://api.github.com/repos/${owner}/${repo}/stats/contributors`, @@ -18,10 +18,10 @@ {w:3, a:faker.random.number(10000), d:faker.random.number(10000), c:faker.random.number(10000)}, {w:4, a:faker.random.number(10000), d:faker.random.number(10000), c:faker.random.number(10000)}, ], - author: { + author:{ login:owner, - } - } - ] + }, + }, + ], }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/rest/repos/getViews.mjs b/source/app/mocks/api/github/rest/repos/getViews.mjs index e16aacc1..617e5399 100644 --- a/source/app/mocks/api/github/rest/repos/getViews.mjs +++ b/source/app/mocks/api/github/rest/repos/getViews.mjs @@ -1,6 +1,6 @@ -/** Mocked data */ +/**Mocked data */ export default function({faker}, target, that, [{owner, repo}]) { - console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.getViews`) + console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getViews") const count = faker.random.number(10000)*2 const uniques = faker.random.number(count)*2 return ({ @@ -17,7 +17,7 @@ views:[ {timestamp:`${faker.date.recent()}`, count:count/2, uniques:uniques/2}, {timestamp:`${faker.date.recent()}`, count:count/2, uniques:uniques/2}, - ] - } + ], + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/rest/repos/listCommits.mjs b/source/app/mocks/api/github/rest/repos/listCommits.mjs index b2deb819..4f6a89d1 100644 --- a/source/app/mocks/api/github/rest/repos/listCommits.mjs +++ b/source/app/mocks/api/github/rest/repos/listCommits.mjs @@ -1,28 +1,26 @@ -/** Mocked data */ +/**Mocked data */ export default function({faker}, target, that, [{page, per_page, owner, repo}]) { - console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.listCommits`) + console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.listCommits") return ({ status:200, url:`https://api.github.com/repos/${owner}/${repo}/commits?per_page=${per_page}&page=${page}`, - headers: { + headers:{ server:"GitHub.com", status:"200 OK", "x-oauth-scopes":"repo", }, - data:page < 2 ? new Array(per_page).fill(null).map(() => - ({ + data:page < 2 ? new Array(per_page).fill(null).map(() => ({ sha:"MOCKED_SHA", commit:{ author:{ name:owner, - date:`${faker.date.recent(14)}` + date:`${faker.date.recent(14)}`, }, committer:{ name:owner, - date:`${faker.date.recent(14)}` + date:`${faker.date.recent(14)}`, }, - } - }) - ) : [] + }, + })) : [], }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/rest/repos/listContributors.mjs b/source/app/mocks/api/github/rest/repos/listContributors.mjs index b24d691a..888fb6e1 100644 --- a/source/app/mocks/api/github/rest/repos/listContributors.mjs +++ b/source/app/mocks/api/github/rest/repos/listContributors.mjs @@ -1,10 +1,10 @@ -/** Mocked data */ +/**Mocked data */ export default function({faker}, target, that, [{owner, repo}]) { - console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.listContributors`) + console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.listContributors") return ({ status:200, url:`https://api.github.com/repos/${owner}/${repo}/contributors`, - headers: { + headers:{ server:"GitHub.com", status:"200 OK", "x-oauth-scopes":"repo", @@ -13,6 +13,6 @@ login:faker.internet.userName(), avatar_url:null, contributions:faker.random.number(1000), - })) + })), }) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/rest/request.mjs b/source/app/mocks/api/github/rest/request.mjs index 9187b2bc..54b871ba 100644 --- a/source/app/mocks/api/github/rest/request.mjs +++ b/source/app/mocks/api/github/rest/request.mjs @@ -1,10 +1,10 @@ -/** Mocked data */ +/**Mocked data */ export default function({faker}, target, that, args) { //Arguments const [url] = args //Head request if (/^HEAD .$/.test(url)) { - console.debug(`metrics/compute/mocks > mocking rest api result > rest.request HEAD`) + console.debug("metrics/compute/mocks > mocking rest api result > rest.request HEAD") return ({ status:200, url:"https://api.github.com/", @@ -13,7 +13,7 @@ status:"200 OK", "x-oauth-scopes":"repo", }, - data:undefined + data:undefined, }) } //Commit content @@ -44,16 +44,16 @@ login:faker.internet.userName(), id:faker.random.number(100000000), }, - files: [ + files:[ { sha:"MOCKED_SHA", filename:faker.system.fileName(), - patch:"@@ -0,0 +1,5 @@\n+//Imports\n+ import app from \"./src/app.mjs\"\n+\n+//Start app\n+ await app()\n\\ No newline at end of file" + patch:"@@ -0,0 +1,5 @@\n+//Imports\n+ import app from \"./src/app.mjs\"\n+\n+//Start app\n+ await app()\n\\ No newline at end of file", }, - ] - } + ], + }, }) } return target(...args) - } \ No newline at end of file + } diff --git a/source/app/mocks/api/github/rest/users/getByUsername.mjs b/source/app/mocks/api/github/rest/users/getByUsername.mjs index 453df398..971c3c21 100644 --- a/source/app/mocks/api/github/rest/users/getByUsername.mjs +++ b/source/app/mocks/api/github/rest/users/getByUsername.mjs @@ -1,10 +1,10 @@ -/** Mocked data */ +/**Mocked data */ export default function({faker}, target, that, [{username}]) { - console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.getByUsername`) + console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getByUsername") return ({ status:200, url:`'https://api.github.com/users/${username}/`, - headers: { + headers:{ server:"GitHub.com", status:"200 OK", "x-oauth-scopes":"repo", @@ -13,6 +13,6 @@ login:faker.internet.userName(), avatar_url:null, contributions:faker.random.number(1000), - } + }, }) - } \ No newline at end of file + } diff --git a/source/app/mocks/index.mjs b/source/app/mocks/index.mjs index e4886bc2..055c544e 100644 --- a/source/app/mocks/index.mjs +++ b/source/app/mocks/index.mjs @@ -9,17 +9,17 @@ let mocked = false //Mocking - export default async function ({graphql, rest}) { + export default async function({graphql, rest}) { //Check if already mocked if (mocked) return {graphql, rest} mocked = true - console.debug(`metrics/compute/mocks > mocking`) + console.debug("metrics/compute/mocks > mocking") //Load mocks const __mocks = paths.join(paths.dirname(urls.fileURLToPath(import.meta.url))) - const mock = async ({directory, mocks}) => { + const mock = async({directory, mocks}) => { for (const entry of await fs.readdir(directory)) { if ((await fs.lstat(paths.join(directory, entry))).isDirectory()) { if (!mocks[entry]) @@ -36,7 +36,7 @@ //GraphQL API mocking { //Unmocked - console.debug(`metrics/compute/mocks > mocking graphql api`) + console.debug("metrics/compute/mocks > mocking graphql api") const unmocked = graphql //Mocked graphql = new Proxy(unmocked, { @@ -46,20 +46,21 @@ const login = query.match(/login: "(?.*?)"/)?.groups?.login ?? faker.internet.userName() //Search for mocked query - for (const mocked of Object.keys(mocks.github.graphql)) + for (const mocked of Object.keys(mocks.github.graphql)) { if (new RegExp(`^query ${mocked.replace(/([.]\w)/g, (_, g) => g.toLocaleUpperCase().substring(1)).replace(/^(\w)/g, (_, g) => g.toLocaleUpperCase())} `).test(query)) return mocks.github.graphql[mocked]({faker, query, login}) + } //Unmocked call return target(...args) - } + }, }) } //Rest API mocking { //Unmocked - console.debug(`metrics/compute/mocks > mocking rest api`) + console.debug("metrics/compute/mocks > mocking rest api") const unmocked = {} //Mocked const mocker = ({path = "rest", mocks, mocked}) => { @@ -79,12 +80,12 @@ //Axios mocking { //Unmocked - console.debug(`metrics/compute/mocks > mocking axios`) + console.debug("metrics/compute/mocks > mocking axios") const unmocked = {get:axios.get, post:axios.post} //Mocked post requests axios.post = new Proxy(unmocked.post, { - apply:function(target, that, args) { + apply(target, that, args) { //Arguments const [url, body] = args @@ -97,12 +98,12 @@ //Unmocked call return target(...args) - } + }, }) //Mocked get requests axios.get = new Proxy(unmocked.get, { - apply:function(target, that, args) { + apply(target, that, args) { //Arguments const [url, options] = args @@ -115,7 +116,7 @@ //Unmocked call return target(...args) - } + }, }) } diff --git a/source/app/web/index.mjs b/source/app/web/index.mjs index 689c4e78..3c00e9e2 100644 --- a/source/app/web/index.mjs +++ b/source/app/web/index.mjs @@ -2,4 +2,6 @@ import app from "./instance.mjs" //Start app - await app({mock:process.env.USE_MOCKED_DATA, nosettings:process.env.NO_SETTINGS}) \ No newline at end of file + (async function() { + await app({mock:process.env.USE_MOCKED_DATA, nosettings:process.env.NO_SETTINGS}) + })() diff --git a/source/app/web/instance.mjs b/source/app/web/instance.mjs index e2589d72..1a343e2e 100644 --- a/source/app/web/instance.mjs +++ b/source/app/web/instance.mjs @@ -10,8 +10,8 @@ import mocks from "../mocks/index.mjs" import metrics from "../metrics/index.mjs" -/** App */ - export default async function ({mock, nosettings} = {}) { +/**App */ + export default async function({mock, nosettings} = {}) { //Load configuration settings const {conf, Plugins, Templates} = await setup({nosettings}) @@ -39,14 +39,14 @@ } } if (((mock)&&(!conf.settings.token))||(mock === "force")) { - console.debug(`metrics/app > using mocked token`) + console.debug("metrics/app > using mocked token") conf.settings.token = "MOCKED_TOKEN" } if (debug) console.debug(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256})) //Load octokits - const api = {graphql:octokit.graphql.defaults({headers:{authorization: `token ${token}`}}), rest:new OctokitRest.Octokit({auth:token})} + const api = {graphql:octokit.graphql.defaults({headers:{authorization:`token ${token}`}}), rest:new OctokitRest.Octokit({auth:token})} //Apply mocking if needed if (mock) Object.assign(api, await mocks(api)) @@ -60,9 +60,11 @@ if (ratelimiter) { app.set("trust proxy", 1) middlewares.push(ratelimit({ - skip(req, res) { return !!cache.get(req.params.login) }, + skip(req, _res) { + return !!cache.get(req.params.login) + }, message:"Too many requests", - ...ratelimiter + ...ratelimiter, })) } //Cache headers middleware @@ -82,7 +84,7 @@ let requests = {limit:0, used:0, remaining:0, reset:NaN} if (!conf.settings.notoken) { requests = (await rest.rateLimit.get()).data.rate - setInterval(async () => requests = (await rest.rateLimit.get()).data.rate, 30*1000) + setInterval(async() => requests = (await rest.rateLimit.get()).data.rate, 30*1000) } //Web app.get("/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/index.html`)) @@ -117,9 +119,9 @@ app.get("/.js/prism.markdown.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-markdown.min.js`)) //Meta app.get("/.version", limiter, (req, res) => res.status(200).send(conf.package.version)) - app.get("/.requests", limiter, async (req, res) => res.status(200).json(requests)) + app.get("/.requests", limiter, async(req, res) => res.status(200).json(requests)) //Cache - app.get("/.uncache", limiter, async (req, res) => { + app.get("/.uncache", limiter, async(req, res) => { const {token, user} = req.query if (token) { if (actions.flush.has(token)) { @@ -127,10 +129,9 @@ cache.del(actions.flush.get(token)) return res.sendStatus(200) } - else - return res.sendStatus(404) + return res.sendStatus(404) } - else { + { const token = `${Math.random().toString(16).replace("0.", "")}${Math.random().toString(16).replace("0.", "")}` actions.flush.set(token, user) return res.json({token}) @@ -138,7 +139,7 @@ }) //Metrics - app.get("/:login", ...middlewares, async (req, res) => { + app.get("/:login", ...middlewares, async(req, res) => { //Request params const login = req.params.login?.replace(/[\n\r]/g, "") if ((restricted.length)&&(!restricted.includes(login))) { @@ -146,13 +147,12 @@ return res.sendStatus(403) } //Read cached data if possible - //User cached - if ((!debug)&&(cached)&&(cache.get(login))) { - const {rendered, mime} = cache.get(login) - res.header("Content-Type", mime) - res.send(rendered) - return - } + if ((!debug)&&(cached)&&(cache.get(login))) { + const {rendered, mime} = cache.get(login) + res.header("Content-Type", mime) + res.send(rendered) + return + } //Maximum simultaneous users if ((maxusers)&&(cache.size()+1 > maxusers)) { console.debug(`metrics/app/${login} > 503 (maximum users reached)`) @@ -167,8 +167,8 @@ const {rendered, mime} = await metrics({login, q}, { graphql, rest, plugins, conf, die:q["plugins.errors.fatal"] ?? false, - verify:q["verify"] ?? false, - convert:["jpeg", "png"].includes(q["config.output"]) ? q["config.output"] : null + verify:q.verify ?? false, + convert:["jpeg", "png"].includes(q["config.output"]) ? q["config.output"] : null, }, {Plugins, Templates}) //Cache if ((!debug)&&(cached)) @@ -206,6 +206,6 @@ `Max simultaneous users │ ${maxusers ? `${maxusers} users` : "(unrestricted)"}`, `Plugins enabled │ ${enabled.map(({name}) => name).join(", ")}`, `SVG optimization │ ${conf.settings.optimize ?? false}`, - `Server ready !` + "Server ready !", ].join("\n"))) } diff --git a/source/plugins/activity/index.mjs b/source/plugins/activity/index.mjs index 90d46370..6b76dd28 100644 --- a/source/plugins/activity/index.mjs +++ b/source/plugins/activity/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, data, rest, q, account, imports}, {enabled = false} = {}) { + export default async function({login, data, rest, q, account, imports}, {enabled = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met diff --git a/source/plugins/activity/metadata.yml b/source/plugins/activity/metadata.yml index 7fe3eb48..7f2fc465 100644 --- a/source/plugins/activity/metadata.yml +++ b/source/plugins/activity/metadata.yml @@ -34,6 +34,7 @@ inputs: type: array format: comma-separated default: all + example: issue, pr, review, wiki, star values: - all # Display all types of events - comment # Display commits, issues and pull requests comments @@ -48,4 +49,4 @@ inputs: - fork # Display forked repositories - star # Display starred repositories - member # Display collaborators additions - - public # Display repositories made public \ No newline at end of file + - public # Display repositories made public diff --git a/source/plugins/anilist/index.mjs b/source/plugins/anilist/index.mjs index 869d2098..2834198f 100644 --- a/source/plugins/anilist/index.mjs +++ b/source/plugins/anilist/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, data, queries, imports, q, account}, {enabled = false} = {}) { + export default async function({login, data, queries, imports, q, account}, {enabled = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -31,7 +31,7 @@ //Format and save results for (const {name, entries} of lists) { //Format results - const list = await Promise.all(entries.map(async media => await format({media, imports}))) + const list = await Promise.all(entries.map(media => format({media, imports}))) result.lists[type][name.toLocaleLowerCase()] = shuffle ? imports.shuffle(list) : list //Limit results if (limit > 0) { @@ -131,7 +131,7 @@ } } -/** Media formatter */ +/**Media formatter */ async function format({media, imports}) { const {progress, score:userScore, media:{title, description, status, startDate:{year:release}, genres, averageScore, episodes, chapters, type, coverImage:{medium:artwork}}} = media return { @@ -140,6 +140,6 @@ description:description.replace(//g, " "), scores:{user:userScore, community:averageScore}, released:type === "ANIME" ? episodes : chapters, - artwork:artwork ? await imports.imgb64(artwork) : "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" + artwork:artwork ? await imports.imgb64(artwork) : "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", } } diff --git a/source/plugins/anilist/metadata.yml b/source/plugins/anilist/metadata.yml index df08b9d6..37efcfbf 100644 --- a/source/plugins/anilist/metadata.yml +++ b/source/plugins/anilist/metadata.yml @@ -28,6 +28,7 @@ inputs: type: array format: comma-separated default: favorites + example: favorites, watching, characters values: - favorites # Favorites animes and mangas (depending on plugin_anilist_medias values) - watching # Animes in your watching list diff --git a/source/plugins/base/index.mjs b/source/plugins/base/index.mjs index bc31138b..00427100 100644 --- a/source/plugins/base/index.mjs +++ b/source/plugins/base/index.mjs @@ -4,7 +4,7 @@ */ //Setup - export default async function ({login, graphql, data, q, queries, imports}, conf) { + export default async function({login, graphql, data, q, queries, imports}, conf) { //Load inputs console.debug(`metrics/compute/${login}/base > started`) let {repositories, repositories_forks:forks} = imports.metadata.plugins.base.inputs({data, q, account:"bypass"}, {repositories:conf.settings.repositories ?? 100}) @@ -16,7 +16,7 @@ //Base parts (legacy handling for web instance) const defaulted = ("base" in q) ? legacy.converter(q.base) ?? true : true for (const part of conf.settings.plugins.base.parts) - data.base[part] = `base.${part}` in q ? legacy.converter(q[ `base.${part}`]) : defaulted + data.base[part] = `base.${part}` in q ? legacy.converter(q[`base.${part}`]) : defaulted //Iterate through account types for (const account of ["user", "organization"]) { @@ -46,7 +46,8 @@ //Success console.debug(`metrics/compute/${login}/base > graphql query > account ${account} > success`) return {} - } catch (error) { + } + catch (error) { console.debug(`metrics/compute/${login}/base > account ${account} > failed : ${error}`) console.debug(`metrics/compute/${login}/base > checking next account`) } @@ -69,7 +70,7 @@ //Organization organization({login, data}) { console.debug(`metrics/compute/${login}/base > applying postprocessing`) - data.account = "organization", + data.account = "organization" Object.assign(data.user, { isHireable:false, starredRepositories:{totalCount:0}, @@ -107,7 +108,7 @@ repositories:{totalCount:0, totalDiskUsage:0, nodes:[]}, packages:{totalCount:0}, }) - } + }, } //Legacy functions @@ -119,5 +120,5 @@ return false if (Number.isFinite(Number(value))) return !!(Number(value)) - } - } \ No newline at end of file + }, + } diff --git a/source/plugins/core/index.mjs b/source/plugins/core/index.mjs index 8c29ae9a..f457e8d6 100644 --- a/source/plugins/core/index.mjs +++ b/source/plugins/core/index.mjs @@ -4,22 +4,25 @@ */ //Setup - export default async function ({login, q, dflags}, {conf, data, rest, graphql, plugins, queries, account}, {pending, imports}) { + export default async function({login, q, dflags}, {conf, data, rest, graphql, plugins, queries, account}, {pending, imports}) { //Load inputs imports.metadata.plugins.core.inputs({data, account, q}) //Init - const computed = data.computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0, forked:0, releases:0}} + const computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0, forked:0, releases:0}} const avatar = imports.imgb64(data.user.avatarUrl) + data.computed = computed console.debug(`metrics/compute/${login} > formatting common metrics`) //Timezone config if (q["config.timezone"]) { - const timezone = data.config.timezone = {name:q["config.timezone"], offset:0} + const timezone = {name:q["config.timezone"], offset:0} + data.config.timezone = timezone try { timezone.offset = Number(new Date().toLocaleString("fr", {timeZoneName:"short", timeZone:timezone.name}).match(/UTC[+](?\d+)/)?.groups?.offset*60*60*1000) || 0 console.debug(`metrics/compute/${login} > timezone set to ${timezone.name} (${timezone.offset > 0 ? "+" : ""}${Math.round(timezone.offset/(60*60*1000))} hours)`) - } catch { + } + catch { timezone.error = `Failed to use timezone "${timezone.name}"` console.debug(`metrics/compute/${login} > failed to use timezone "${timezone.name}"`) } @@ -35,7 +38,7 @@ for (const name of Object.keys(imports.plugins)) { if (!plugins[name]?.enabled) continue - pending.push((async () => { + pending.push((async() => { try { console.debug(`metrics/compute/${login}/plugins > ${name} > started`) data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql, queries, account}, plugins[name]) @@ -71,7 +74,7 @@ computed.diskUsage = `${imports.bytes(data.user.repositories.totalDiskUsage*1000)}` //Compute licenses stats - computed.licenses.favorite = Object.entries(computed.licenses.used).sort(([an, a], [bn, b]) => b - a).slice(0, 1).map(([name, value]) => name) ?? "" + computed.licenses.favorite = Object.entries(computed.licenses.used).sort(([_an, a], [_bn, b]) => b - a).slice(0, 1).map(([name, _value]) => name) ?? "" //Compute total commits computed.commits += data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount @@ -117,7 +120,7 @@ computed.calendar.map(day => day.color = halloween(day.color)) //Update isocalendar colors const waiting = [...pending] - pending.push((async () => { + pending.push((async() => { await Promise.all(waiting) if (data.plugins.isocalendar?.svg) data.plugins.isocalendar.svg = halloween(data.plugins.isocalendar.svg) @@ -127,4 +130,4 @@ //Results return null - } \ No newline at end of file + } diff --git a/source/plugins/followup/index.mjs b/source/plugins/followup/index.mjs index 03be73ad..93ee9cb5 100644 --- a/source/plugins/followup/index.mjs +++ b/source/plugins/followup/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({data, computed, imports, q, account}, {enabled = false} = {}) { + export default async function({data, computed, imports, q, account}, {enabled = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -12,15 +12,27 @@ //Define getters const followup = { issues:{ - get count() { return this.open + this.closed }, - get open() { return computed.repositories.issues_open }, - get closed() { return computed.repositories.issues_closed }, + get count() { + return this.open + this.closed + }, + get open() { + return computed.repositories.issues_open + }, + get closed() { + return computed.repositories.issues_closed + }, }, pr:{ - get count() { return this.open + this.merged }, - get open() { return computed.repositories.pr_open }, - get merged() { return computed.repositories.pr_merged } - } + get count() { + return this.open + this.merged + }, + get open() { + return computed.repositories.pr_open + }, + get merged() { + return computed.repositories.pr_merged + }, + }, } //Results @@ -30,4 +42,4 @@ catch (error) { throw {error:{message:"An error occured", instance:error}} } - } \ No newline at end of file + } diff --git a/source/plugins/gists/index.mjs b/source/plugins/gists/index.mjs index c1d5fa59..8cba9330 100644 --- a/source/plugins/gists/index.mjs +++ b/source/plugins/gists/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, data, graphql, q, imports, queries, account}, {enabled = false} = {}) { + export default async function({login, data, graphql, q, imports, queries, account}, {enabled = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -28,7 +28,7 @@ //Iterate through gists console.debug(`metrics/compute/${login}/plugins > gists > processing ${gists.length} gists`) - let stargazers = 0, forks = 0, comments = 0, files = 0 + let comments = 0, files = 0, forks = 0, stargazers = 0 for (const gist of gists) { //Skip forks if (gist.isFork) diff --git a/source/plugins/habits/index.mjs b/source/plugins/habits/index.mjs index adede1a1..2af642e1 100644 --- a/source/plugins/habits/index.mjs +++ b/source/plugins/habits/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, data, rest, imports, q, account}, {enabled = false, ...defaults} = {}) { + export default async function({login, data, rest, imports, q, account}, {enabled = false, ...defaults} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -22,7 +22,10 @@ console.debug(`metrics/compute/${login}/plugins > habits > loading page ${page}`) events.push(...(await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data) } - } catch { console.debug(`metrics/compute/${login}/plugins > habits > no more page to load`) } + } + catch { + console.debug(`metrics/compute/${login}/plugins > habits > no more page to load`) + } console.debug(`metrics/compute/${login}/plugins > habits > ${events.length} events loaded`) //Get user recent commits @@ -36,8 +39,7 @@ console.debug(`metrics/compute/${login}/plugins > habits > loading patches`) const patches = [...await Promise.allSettled(commits .flatMap(({payload}) => payload.commits).map(commit => commit.url) - .map(async commit => (await rest.request(commit)).data.files) - )] + .map(async commit => (await rest.request(commit)).data.files))] .filter(({status}) => status === "fulfilled") .map(({value}) => value) .flatMap(files => files.map(file => ({name:imports.paths.basename(file.filename), patch:file.patch ?? ""}))) @@ -52,7 +54,7 @@ habits.commits.days[day] = (habits.commits.days[day] ?? 0) + 1 habits.commits.days.max = Math.max(...Object.values(habits.commits.days)) //Compute day with most commits - habits.commits.day = days.length ? ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][Object.entries(habits.commits.days).sort(([an, a], [bn, b]) => b - a).map(([day, occurence]) => day)[0]] ?? NaN : NaN + habits.commits.day = days.length ? ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][Object.entries(habits.commits.days).sort(([_an, a], [_bn, b]) => b - a).map(([day, _occurence]) => day)[0]] ?? NaN : NaN } //Commit hour @@ -64,7 +66,7 @@ habits.commits.hours[hour] = (habits.commits.hours[hour] ?? 0) + 1 habits.commits.hours.max = Math.max(...Object.values(habits.commits.hours)) //Compute hour with most commits - habits.commits.hour = hours.length ? `${Object.entries(habits.commits.hours).sort(([an, a], [bn, b]) => b - a).map(([hour, occurence]) => hour)[0]}`.padStart(2, "0") : NaN + habits.commits.hour = hours.length ? `${Object.entries(habits.commits.hours).sort(([_an, a], [_bn, b]) => b - a).map(([hour, _occurence]) => hour)[0]}`.padStart(2, "0") : NaN } //Indent style @@ -72,7 +74,7 @@ //Attempt to guess whether tabs or spaces are used in patches console.debug(`metrics/compute/${login}/plugins > habits > searching indent style`) patches - .map(({patch}) => patch.match(/((?:\t)|(?: )) /gm) ?? []) + .map(({patch}) => patch.match(/((?:\t)|(?:[ ]{2})) /gm) ?? []) //eslint-disable-line prefer-named-capture-group .forEach(indent => habits.indents[/^\t/.test(indent) ? "tabs" : "spaces"]++) habits.indents.style = habits.indents.spaces > habits.indents.tabs ? "spaces" : habits.indents.tabs > habits.indents.spaces ? "tabs" : "" } @@ -89,18 +91,18 @@ //Create temporary directory and save patches console.debug(`metrics/compute/${login}/plugins > habits > creating temp dir ${path} with ${patches.length} files`) await imports.fs.mkdir(path, {recursive:true}) - await Promise.all(patches.map(async ({name, patch}, i) => await imports.fs.writeFile(imports.paths.join(path, `${i}${imports.paths.extname(name)}`), patch))) + await Promise.all(patches.map(({name, patch}, i) => imports.fs.writeFile(imports.paths.join(path, `${i}${imports.paths.extname(name)}`), patch))) //Create temporary git repository console.debug(`metrics/compute/${login}/plugins > habits > creating temp git repository`) - await imports.run(`git init && git add . && git config user.name "linguist" && git config user.email "<>" && git commit -m "linguist"`, {cwd:path}).catch(console.debug) - await imports.run(`git status`, {cwd:path}) + await imports.run('git init && git add . && git config user.name "linguist" && git config user.email "<>" && git commit -m "linguist"', {cwd:path}).catch(console.debug) + await imports.run("git status", {cwd:path}) //Spawn linguist process console.debug(`metrics/compute/${login}/plugins > habits > running linguist`) ;(await imports.run(`${prefix} github-linguist --breakdown`, {cwd:path})) //Parse linguist result .split("\n").map(line => line.match(/(?[\d.]+)%\s+(?\w+)/)?.groups).filter(line => line) .map(({value, language}) => habits.linguist.languages[language] = (habits.linguist.languages[language] ?? 0) + value/100) - habits.linguist.ordered = Object.entries(habits.linguist.languages).sort(([an, a], [bn, b]) => b - a) + habits.linguist.ordered = Object.entries(habits.linguist.languages).sort(([_an, a], [_bn, b]) => b - a) } else console.debug(`metrics/compute/${login}/plugins > habits > linguist not available`) @@ -115,4 +117,4 @@ throw error throw {error:{message:"An error occured", instance:error}} } - } \ No newline at end of file + } diff --git a/source/plugins/isocalendar/index.mjs b/source/plugins/isocalendar/index.mjs index 2bc6b563..acab4a38 100644 --- a/source/plugins/isocalendar/index.mjs +++ b/source/plugins/isocalendar/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, data, graphql, q, imports, queries, account}, {enabled = false} = {}) { + export default async function({login, data, graphql, q, imports, queries, account}, {enabled = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -39,7 +39,7 @@ //Compute the highest contributions in a day, streaks and average commits per day console.debug(`metrics/compute/${login}/plugins > isocalendar > computing stats`) - let max = 0, streak = {max:0, current:0}, values = [], average = 0 + let average = 0, max = 0, streak = {max:0, current:0}, values = [] for (const week of calendar.weeks) { for (const day of week.contributionDays) { values.push(day.contributionCount) @@ -61,8 +61,8 @@ ${[..."RGB"].map(channel => ``).join("")} - ` - ).join("")} + `) + .join("")} ` //Iterate through weeks for (const week of calendar.weeks) { @@ -79,10 +79,10 @@ ` j++ } - svg += `` + svg += "" i++ } - svg += `` + svg += "" //Results return {streak, max, average, svg, duration} @@ -93,4 +93,4 @@ throw error throw {error:{message:"An error occured", instance:error}} } - } \ No newline at end of file + } diff --git a/source/plugins/languages/index.mjs b/source/plugins/languages/index.mjs index ece9f246..77fac98a 100644 --- a/source/plugins/languages/index.mjs +++ b/source/plugins/languages/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, data, imports, q, account}, {enabled = false} = {}) { + export default async function({login, data, imports, q, account}, {enabled = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -42,7 +42,7 @@ //Compute languages stats console.debug(`metrics/compute/${login}/plugins > languages > computing stats`) - languages.favorites = Object.entries(languages.stats).sort(([an, a], [bn, b]) => b - a).slice(0, 8).map(([name, value]) => ({name, value, size:value, color:languages.colors[name], x:0})).filter(({value}) => value/languages.total > threshold) + languages.favorites = Object.entries(languages.stats).sort(([_an, a], [_bn, b]) => b - a).slice(0, 8).map(([name, value]) => ({name, value, size:value, color:languages.colors[name], x:0})).filter(({value}) => value/languages.total > threshold) const visible = {total:Object.values(languages.favorites).map(({size}) => size).reduce((a, b) => a + b, 0)} for (let i = 0; i < languages.favorites.length; i++) { languages.favorites[i].value /= visible.total diff --git a/source/plugins/languages/metadata.yml b/source/plugins/languages/metadata.yml index 3ad4a788..a8fcb5a5 100644 --- a/source/plugins/languages/metadata.yml +++ b/source/plugins/languages/metadata.yml @@ -18,6 +18,7 @@ inputs: type: array format: comma-separated default: "" + example: html, css, ... # List of repositories that will be skipped plugin_languages_skipped: @@ -25,6 +26,7 @@ inputs: type: array format: comma-separated default: "" + example: my-repo-1, my-repo-2, ... # Overrides default languages colors # Use `${n}:${color}` to change the color of the n-th most used language (e.g. "0:red" to make your most used language red) @@ -38,6 +40,7 @@ inputs: - comma-separated - /((?[0-9])|(?[-+a-z0-9#])):(?#?[-a-z0-9]+)/ default: github + example: javascript:red, 0:blue, 1:#ff00aa # Languages additional details plugin_languages_details: @@ -48,6 +51,7 @@ inputs: - bytes-size # Languages total size written in bytes - percentage # Languages proportions in % default: "" + example: bytes-size, percentage # Minimum threshold (in percentage) to reach for languages to be displayed plugin_languages_threshold: diff --git a/source/plugins/lines/index.mjs b/source/plugins/lines/index.mjs index ce7e5910..f6c71c97 100644 --- a/source/plugins/lines/index.mjs +++ b/source/plugins/lines/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, data, imports, rest, q, account}, {enabled = false} = {}) { + export default async function({login, data, imports, rest, q, account}, {enabled = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -22,7 +22,7 @@ //Get contributors stats from repositories console.debug(`metrics/compute/${login}/plugins > lines > querying api`) const lines = {added:0, deleted:0} - const response = await Promise.all(repositories.map(async ({repo, owner}) => await rest.repos.getContributorsStats({owner, repo}))) + const response = await Promise.all(repositories.map(({repo, owner}) => rest.repos.getContributorsStats({owner, repo}))) //Compute changed lines console.debug(`metrics/compute/${login}/plugins > lines > computing total diff`) diff --git a/source/plugins/music/README.md b/source/plugins/music/README.md index 3b14b601..4bb5c1ac 100644 --- a/source/plugins/music/README.md +++ b/source/plugins/music/README.md @@ -180,8 +180,8 @@ Register your API key to finish setup. plugin_music_provider: spotify # Use Spotify as provider plugin_music_mode: recent # Set plugin mode plugin_music_limit: 4 # Limit to 4 entries + plugin_music_played_at: yes # Show timestamp (only works with spotify, 🚧 @master feature) plugin_music_token: "${{ secrets.SPOTIFY_CLIENT_ID }}, ${{ secrets.SPOTIFY_CLIENT_SECRET }}, ${{ secrets.SPOTIFY_REFRESH_TOKEN }}" - plugin_music_played_at: yes # Only works with spotify. ``` ```yaml diff --git a/source/plugins/music/index.mjs b/source/plugins/music/index.mjs index fb44412c..0558fb45 100644 --- a/source/plugins/music/index.mjs +++ b/source/plugins/music/index.mjs @@ -20,7 +20,7 @@ } //Setup - export default async function ({login, imports, data, q, account}, {enabled = false, token = ""} = {}) { + export default async function({login, imports, data, q, account}, {enabled = false, token = ""} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -29,34 +29,40 @@ //Initialization const raw = { - get provider() { return providers[provider]?.name ?? "" }, - get mode() { return modes[mode] ?? "Unconfigured music plugin"}, + get provider() { + return providers[provider]?.name ?? "" + }, + get mode() { + return modes[mode] ?? "Unconfigured music plugin" + }, } let tracks = null //Load inputs - let {provider, mode, playlist, limit, user, played_at} = imports.metadata.plugins.music.inputs({data, account, q}) + let {provider, mode, playlist, limit, user, "played.at":played_at} = imports.metadata.plugins.music.inputs({data, account, q}) //Auto-guess parameters if ((playlist)&&(!mode)) mode = "playlist" - if ((playlist)&&(!provider)) - for (const [name, {embed}] of Object.entries(providers)) + if ((playlist)&&(!provider)) { + for (const [name, {embed}] of Object.entries(providers)) { if (embed.test(playlist)) provider = name + } + } if (!mode) mode = "recent" //Provider if (!(provider in providers)) - throw {error:{message:provider ? `Unsupported provider "${provider}"` : `Missing provider`}, ...raw} + throw {error:{message:provider ? `Unsupported provider "${provider}"` : "Missing provider"}, ...raw} //Mode if (!(mode in modes)) throw {error:{message:`Unsupported mode "${mode}"`}, ...raw} //Playlist mode if (mode === "playlist") { if (!playlist) - throw {error:{message:`Missing playlist url`}, ...raw} + throw {error:{message:"Missing playlist url"}, ...raw} if (!providers[provider].embed.test(playlist)) - throw {error:{message:`Unsupported playlist url format`}, ...raw} + throw {error:{message:"Unsupported playlist url format"}, ...raw} } //Limit limit = Math.max(1, Math.min(100, Number(limit))) @@ -83,7 +89,7 @@ tracks = [...await frame.evaluate(() => [...document.querySelectorAll(".tracklist li")].map(li => ({ name:li.querySelector(".tracklist__track__name").innerText, artist:li.querySelector(".tracklist__track__sub").innerText, - artwork:li.querySelector(".tracklist__track__artwork img").src + artwork:li.querySelector(".tracklist__track__artwork img").src, })))] break } @@ -95,7 +101,7 @@ name:tr.querySelector("td:nth-child(2) div:nth-child(1)").innerText, artist:tr.querySelector("td:nth-child(2) div:nth-child(2)").innerText, //Spotify doesn't provide artworks so we fallback on playlist artwork instead - artwork:window.getComputedStyle(document.querySelector("button[title=Play]").parentNode, null).backgroundImage.match(/^url\("(https:...+)"\)$/)[1] + artwork:window.getComputedStyle(document.querySelector("button[title=Play]").parentNode, null).backgroundImage.match(/^url\("(?https:...+)"\)$/)?.groups?.url ?? null, })))] break } @@ -118,8 +124,6 @@ } //Recently played case "recent":{ - //Initialisation - const timestamp = Date.now()-24*60*60*1000 //Handle provider switch (provider) { //Spotify @@ -127,28 +131,40 @@ //Prepare credentials const [client_id, client_secret, refresh_token] = token.split(",").map(part => part.trim()) if ((!client_id)||(!client_secret)||(!refresh_token)) - throw {error:{message:`Spotify token must contain client id/secret and refresh token`}} + throw {error:{message:"Spotify token must contain client id/secret and refresh token"}} //API call and parse tracklist try { //Request access token console.debug(`metrics/compute/${login}/plugins > music > requesting access token with spotify refresh token`) - const {data:{access_token:access}} = await imports.axios.post("https://accounts.spotify.com/api/token", - `${new imports.url.URLSearchParams({grant_type:"refresh_token", refresh_token, client_id, client_secret})}`, - {headers:{"Content-Type":"application/x-www-form-urlencoded"}}, - ) + const {data:{access_token:access}} = await imports.axios.post("https://accounts.spotify.com/api/token", `${new imports.url.URLSearchParams({grant_type:"refresh_token", refresh_token, client_id, client_secret})}`, {headers:{ + "Content-Type":"application/x-www-form-urlencoded", + }}) console.debug(`metrics/compute/${login}/plugins > music > got access token`) //Retrieve tracks console.debug(`metrics/compute/${login}/plugins > music > querying spotify api`) - tracks = (await imports.axios.get(`https://api.spotify.com/v1/me/player/recently-played?limit=${limit}&after=${timestamp}`, {headers:{ - "Accept":"application/json", - "Content-Type":"application/json", - "Authorization":`Bearer ${access}`} - })).data.items.map(({track, played_at}) => ({ - name:track.name, - artist:track.artists[0].name, - artwork:track.album.images[0].url, - played_at: played_at ? imports.dayjs(played_at).format('[played at] HH:MM on DD/MM/YYYY') : '' - })) + tracks = [] + for (let hours = .5; hours <= 24; hours++) { + //Load track half-hour by half-hour + const timestamp = Date.now()-hours*60*60*1000 + const loaded = (await imports.axios.get(`https://api.spotify.com/v1/me/player/recently-played?after=${timestamp}`, {headers:{ + "Content-Type":"application/json", + Accept:"application/json", + Authorization:`Bearer ${access}`, + }})).data.items.map(({track, played_at}) => ({ + name:track.name, + artist:track.artists[0].name, + artwork:track.album.images[0].url, + played_at:played_at ? imports.dayjs(played_at).format("[played at] HH:MM on DD/MM/YYYY") : null, + })) + //Ensure no duplicate are added + for (const track of loaded) { + if (!tracks.map(({name}) => name).includes(track.name)) + tracks.push(track) + } + //Early break + if (tracks.length >= limit) + break + } } //Handle errors catch (error) { @@ -169,9 +185,9 @@ try { console.debug(`metrics/compute/${login}/plugins > music > querying lastfm api`) tracks = (await imports.axios.get(`https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${user}&api_key=${token}&limit=${limit}&format=json`, {headers:{ - "Accept":"application/json", - "User-Agent":"lowlighter/metrics"} - })).data.recenttracks.track.map((track) => ({ + "User-Agent":"lowlighter/metrics", + Accept:"application/json", + }})).data.recenttracks.track.map(track => ({ name:track.name, artist:track.artist["#text"], artwork:track.image.reverse()[0]["#text"], @@ -215,11 +231,11 @@ track.artwork = await imports.imgb64(track.artwork) } //Save results - return {...raw, tracks} + return {...raw, tracks, played_at} } //Unhandled error - throw {error:{message:`An error occured (could not retrieve tracks)`}} + throw {error:{message:"An error occured (could not retrieve tracks)"}} } //Handle errors catch (error) { @@ -227,4 +243,4 @@ throw error throw {error:{message:"An error occured", instance:error}} } - } \ No newline at end of file + } diff --git a/source/plugins/music/metadata.yml b/source/plugins/music/metadata.yml index fbdeada8..036d0f17 100644 --- a/source/plugins/music/metadata.yml +++ b/source/plugins/music/metadata.yml @@ -47,6 +47,7 @@ inputs: description: Embed playlist url type: string default: "" + example: https://embed.music.apple.com/--/playlist/--------/-------- # Number of music tracks to display plugin_music_limit: @@ -55,6 +56,8 @@ inputs: default: 4 min: 1 max: 100 + + # Display when track was last played plugin_music_played_at: description: Display when the track was played type: boolean diff --git a/source/plugins/music/tests.yml b/source/plugins/music/tests.yml index 6312605f..4d7411b0 100644 --- a/source/plugins/music/tests.yml +++ b/source/plugins/music/tests.yml @@ -18,6 +18,7 @@ token: NOT_NEEDED plugin_music_token: MOCKED_CLIENT_ID, MOCKED_CLIENT_SECRET, MOCKED_REFRESH_TOKEN plugin_music: yes + plugin_music_played_at: yes plugin_music_provider: spotify - name: Music plugin (recent - lastfm) diff --git a/source/plugins/pagespeed/index.mjs b/source/plugins/pagespeed/index.mjs index a74a219d..2c7e33af 100644 --- a/source/plugins/pagespeed/index.mjs +++ b/source/plugins/pagespeed/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, imports, data, q, account}, {enabled = false, token = null} = {}) { + export default async function({login, imports, data, q, account}, {enabled = false, token = null} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -53,4 +53,4 @@ } throw {error:{message, instance:error}} } - } \ No newline at end of file + } diff --git a/source/plugins/people/index.mjs b/source/plugins/people/index.mjs index 65151883..adb62c32 100644 --- a/source/plugins/people/index.mjs +++ b/source/plugins/people/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, data, graphql, rest, q, queries, imports, account}, {enabled = false} = {}) { + export default async function({login, data, graphql, rest, q, queries, imports, account}, {enabled = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -12,7 +12,7 @@ types:account === "organization" ? ["sponsorshipsAsMaintainer", "sponsorshipsAsSponsor", "thanks"] : ["followers", "following", "sponsorshipsAsMaintainer", "sponsorshipsAsSponsor", "thanks"], default:"followers, following", alias:{followed:"following", sponsors:"sponsorshipsAsMaintainer", sponsored:"sponsorshipsAsSponsor", sponsoring:"sponsorshipsAsSponsor"}, - sponsorships:{sponsorshipsAsMaintainer:"sponsorEntity", sponsorshipsAsSponsor:"sponsorable"} + sponsorships:{sponsorshipsAsMaintainer:"sponsorEntity", sponsorshipsAsSponsor:"sponsorable"}, } if (q.repo) { console.debug(`metrics/compute/${login}/plugins > people > switched to repository mode`) diff --git a/source/plugins/people/metadata.yml b/source/plugins/people/metadata.yml index 552e7b06..eb7d5115 100644 --- a/source/plugins/people/metadata.yml +++ b/source/plugins/people/metadata.yml @@ -34,6 +34,7 @@ inputs: type: array format: comma-separated default: followers, following + example: follower, following, sponsors, sponsoring values: - followers # For user metrics - following # For user metrics diff --git a/source/plugins/posts/index.mjs b/source/plugins/posts/index.mjs index aa85ef5f..4465fd3f 100644 --- a/source/plugins/posts/index.mjs +++ b/source/plugins/posts/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, data, imports, q, account}, {enabled = false} = {}) { + export default async function({login, data, imports, q, account}, {enabled = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -36,7 +36,7 @@ } //Unhandled error - throw {error:{message:`An error occured (could not retrieve posts)`}} + throw {error:{message:"An error occured (could not retrieve posts)"}} } //Handle errors catch (error) { @@ -44,4 +44,4 @@ throw error throw {error:{message:"An error occured", instance:error}} } - } \ No newline at end of file + } diff --git a/source/plugins/projects/index.mjs b/source/plugins/projects/index.mjs index 9379a0c9..b2ffe005 100644 --- a/source/plugins/projects/index.mjs +++ b/source/plugins/projects/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, data, imports, graphql, q, queries, account}, {enabled = false} = {}) { + export default async function({login, data, imports, graphql, q, queries, account}, {enabled = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -21,7 +21,7 @@ for (const identifier of repositories) { //Querying repository project console.debug(`metrics/compute/${login}/plugins > projects > querying api for ${identifier}`) - const {user, repository, id} = identifier.match(/(?[-\w]+)[/](?[-\w]+)[/]projects[/](?\d+)/)?.groups + const {user, repository, id} = identifier.match(/(?[-\w]+)[/](?[-\w]+)[/]projects[/](?\d+)/)?.groups ?? {} const {[account]:{repository:{project}}} = await graphql(queries.projects.repository({user, repository, id, account})) //Adding it to projects list console.debug(`metrics/compute/${login}/plugins > projects > registering ${identifier}`) diff --git a/source/plugins/projects/metadata.yml b/source/plugins/projects/metadata.yml index dd8963c7..e22ee3e9 100644 --- a/source/plugins/projects/metadata.yml +++ b/source/plugins/projects/metadata.yml @@ -27,6 +27,7 @@ inputs: plugin_projects_repositories: description: List of repository project identifiers to disaplay type: array + example: username/repo/projects/1, username/repo/projects/2, ... format: - comma-separated - /(?[-a-z0-9]+)[/](?[-a-z0-9]+)[/]projects[/](?[0-9]+)/ diff --git a/source/plugins/stargazers/index.mjs b/source/plugins/stargazers/index.mjs index 1d2ff4ea..c2b73b29 100644 --- a/source/plugins/stargazers/index.mjs +++ b/source/plugins/stargazers/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, graphql, data, imports, q, queries, account}, {enabled = false} = {}) { + export default async function({login, graphql, data, imports, q, queries, account}, {enabled = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -41,7 +41,7 @@ increments.max = Math.max(...Object.values(increments.dates)) //Compute total stargazers - let stargazers = data.computed.repositories.stargazers + let {stargazers} = data.computed.repositories const total = {dates:{...increments.dates}, max:NaN, min:NaN} { const dates = Object.keys(total.dates) diff --git a/source/plugins/stars/index.mjs b/source/plugins/stars/index.mjs index c3aa6ebd..916992fa 100644 --- a/source/plugins/stars/index.mjs +++ b/source/plugins/stars/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, data, graphql, q, queries, imports, account}, {enabled = false} = {}) { + export default async function({login, data, graphql, q, queries, imports, account}, {enabled = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met diff --git a/source/plugins/topics/index.mjs b/source/plugins/topics/index.mjs index 8eaa1488..e547b956 100644 --- a/source/plugins/topics/index.mjs +++ b/source/plugins/topics/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, data, imports, q, account}, {enabled = false} = {}) { + export default async function({login, data, imports, q, account}, {enabled = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -90,4 +90,4 @@ throw error throw {error:{message:"An error occured", instance:error}} } - } \ No newline at end of file + } diff --git a/source/plugins/traffic/index.mjs b/source/plugins/traffic/index.mjs index ff894d2f..84f1e43a 100644 --- a/source/plugins/traffic/index.mjs +++ b/source/plugins/traffic/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, imports, data, rest, q, account}, {enabled = false} = {}) { + export default async function({login, imports, data, rest, q, account}, {enabled = false} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -15,7 +15,7 @@ //Get views stats from repositories console.debug(`metrics/compute/${login}/plugins > traffic > querying api`) const views = {count:0, uniques:0} - const response = await Promise.all(repositories.map(async ({repo, owner}) => await rest.repos.getViews({owner, repo}))) + const response = await Promise.all(repositories.map(({repo, owner}) => rest.repos.getViews({owner, repo}))) //Compute views console.debug(`metrics/compute/${login}/plugins > traffic > computing stats`) @@ -31,4 +31,4 @@ message = "Insufficient token rights" throw {error:{message, instance:error}} } - } \ No newline at end of file + } diff --git a/source/plugins/tweets/index.mjs b/source/plugins/tweets/index.mjs index 98447394..0173304c 100644 --- a/source/plugins/tweets/index.mjs +++ b/source/plugins/tweets/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, imports, data, q, account}, {enabled = false, token = ""} = {}) { + export default async function({login, imports, data, q, account}, {enabled = false, token = ""} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -35,18 +35,17 @@ tweet.mentions = tweet.entities?.mentions.map(({username}) => username) ?? [] //Format text console.debug(`metrics/compute/${login}/plugins > tweets > formatting tweet ${tweet.id}`) - tweet.text = imports.htmlescape( + tweet.text = imports.htmlescape( //eslint-disable-line function-paren-newline //Escape tags imports.htmlescape(tweet.text, {"<":true, ">":true}) //Mentions - .replace(new RegExp(`@(${tweet.mentions.join("|")})`, "gi"), ` @$1 `) + .replace(new RegExp(`@(${tweet.mentions.join("|")})`, "gi"), ' @$1 ') //Hashtags (this regex comes from the twitter source code) - .replace(/(?#$1 `) + .replace(/(?#$1 ') //eslint-disable-line no-misleading-character-class, prefer-named-capture-group //Line breaks .replace(/\n/g, "
") //Links - .replace(/https?:[/][/](t.co[/]\w+)/g, ` $1 `) - , {"&":true}) + .replace(/https?:[/][/](?t.co[/]\w+)/g, ' $ '), {"&":true}) })) //Result @@ -63,4 +62,4 @@ } throw {error:{message, instance:error}} } - } \ No newline at end of file + } diff --git a/source/plugins/wakatime/index.mjs b/source/plugins/wakatime/index.mjs index c29d0abc..26089b61 100644 --- a/source/plugins/wakatime/index.mjs +++ b/source/plugins/wakatime/index.mjs @@ -1,5 +1,5 @@ //Setup - export default async function ({login, q, imports, data, account}, {enabled = false, token} = {}) { + export default async function({login, q, imports, data, account}, {enabled = false, token} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met @@ -12,8 +12,7 @@ limit = void(limit) const range = {"7":"last_7_days", "30":"last_30_days", "180":"last_6_months", "365":"last_year"}[days] ?? "last_7_days" - //Querying api and format result - //https://wakatime.com/developers#stats + //Querying api and format result (https://wakatime.com/developers#stats) console.debug(`metrics/compute/${login}/plugins > wakatime > querying api`) const {data:{data:stats}} = await imports.axios.get(`https://wakatime.com/api/v1/users/current/stats/${range}?api_key=${token}`) const result = { @@ -42,4 +41,4 @@ } throw {error:{message, instance:error}} } - } \ No newline at end of file + } diff --git a/source/templates/classic/partials/music.ejs b/source/templates/classic/partials/music.ejs index bb1ca96e..7c92ddd0 100644 --- a/source/templates/classic/partials/music.ejs +++ b/source/templates/classic/partials/music.ejs @@ -22,12 +22,12 @@
<% for (const {name = "", artist = "", artwork = "", played_at = ""} of plugins.music.tracks) { %>
- +
-
<%= name %>
+
<%= name %>
<%= artist %>
- <% if (played_at.length) { %> -
<%= played_at %>
+ <% if (plugins.music.played_at) { %> +
<%= played_at %>
<% } %>
diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css index 2bd3e624..db27b723 100644 --- a/source/templates/classic/style.css +++ b/source/templates/classic/style.css @@ -272,8 +272,9 @@ .track .name { font-size: 14px; line-height: 14px; + font-weight: 600; } - .track .artist { + .track .artist, .track .played-at { font-size: 12px; color: #666666; } diff --git a/source/templates/classic/template.mjs b/source/templates/classic/template.mjs index 515544fa..70a2940e 100644 --- a/source/templates/classic/template.mjs +++ b/source/templates/classic/template.mjs @@ -1,5 +1,5 @@ -/** Template processor */ - export default async function ({login, q}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) { +/**Template processor */ + export default async function(_, __, {imports}) { //Core await imports.plugins.core(...arguments) - } \ No newline at end of file + } diff --git a/source/templates/repository/template.mjs b/source/templates/repository/template.mjs index cd9d4ca5..06eaed28 100644 --- a/source/templates/repository/template.mjs +++ b/source/templates/repository/template.mjs @@ -1,11 +1,11 @@ -/** Template processor */ - export default async function ({login, q}, {conf, data, rest, graphql, plugins, queries, account}, {s, pending, imports}) { +/**Template processor */ + export default async function({login, q}, {data, rest, graphql, queries, account}, {pending, imports}) { //Check arguments const {repo} = q if (!repo) { console.debug(`metrics/compute/${login}/${repo} > error, repo was undefined`) - data.errors.push({error:{message:`You must pass a "repo" argument to use this template`}}) - return await imports.plugins.core(...arguments) + data.errors.push({error:{message:"You must pass a \"repo\" argument to use this template"}}) + return imports.plugins.core(...arguments) } console.debug(`metrics/compute/${login}/${repo} > switching to mode ${account}`) @@ -73,4 +73,4 @@ //Reformat projects names if (data.plugins.projects) data.plugins.projects.list?.map(project => project.name = project.name.replace(`(${login}/${repo})`, "").trim()) - } \ No newline at end of file + } diff --git a/source/templates/terminal/template.mjs b/source/templates/terminal/template.mjs index 17e26ff6..a4257e8a 100644 --- a/source/templates/terminal/template.mjs +++ b/source/templates/terminal/template.mjs @@ -1,7 +1,7 @@ -/** Template processor */ - export default async function ({login, q}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) { +/**Template processor */ + export default async function({q}, _, {imports}) { //Core await imports.plugins.core(...arguments) //Disable optimization to keep white-spaces q.raw = true - } \ No newline at end of file + }
See documentation 🌍