diff --git a/.editorconfig b/.editorconfig index 81eba8cc..9c61627f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,9 @@ root = true [*] +charset = utf-8 +end_of_line = lf indent_style = space indent_size = 4 -charset = utf-8 -trim_trailing_whitespace = true insert_final_newline = true -end_of_line = lf +trim_trailing_whitespace = true diff --git a/.eslintrc.js b/.eslintrc.js index 298442d8..c70176a6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,42 +1,112 @@ module.exports = { - root: true, - plugins: ['@typescript-eslint', 'promise', 'import', 'jsdoc'], env: { - node: true, + browser: true, es6: true, - browser: true + node: true }, extends: [ 'eslint:recommended', - 'plugin:promise/recommended', - 'plugin:@typescript-eslint/recommended', 'plugin:jsdoc/recommended', + 'plugin:json/recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier', + 'plugin:prettier/recommended', + 'plugin:promise/recommended', 'plugin:import/errors', 'plugin:import/warnings', - 'plugin:import/typescript', - 'prettier' + 'plugin:import/typescript' ], - rules: { - '@typescript-eslint/explicit-function-return-type': 'error', - '@typescript-eslint/no-explicit-any': 'warn', - 'import/newline-after-import': 'error', - 'import/order': 'error' - }, overrides: [ { - files: ['.js', '.ts'], env: { - node: false, browser: true, - es6: true + es6: true, + node: false }, + files: ['.js', '.ts'], globals: { + $scope: 'writable', cast: 'readonly', - PRODUCTION: 'readonly', - $scope: 'writable' + PRODUCTION: 'readonly' } } ], + plugins: ['prettier', 'promise', 'import', 'jsdoc'], + root: true, + rules: { + '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/prefer-ts-expect-error': 'error', + curly: 'error', + 'import/newline-after-import': 'error', + 'import/order': 'error', + 'jsdoc/check-indentation': 'error', + 'jsdoc/check-param-names': 'error', + 'jsdoc/check-property-names': 'error', + 'jsdoc/check-syntax': 'error', + 'jsdoc/check-tag-names': 'error', + 'jsdoc/no-types': 'error', + 'jsdoc/require-description': 'warn', + 'jsdoc/require-hyphen-before-param-description': 'error', + 'jsdoc/require-jsdoc': 'error', + 'jsdoc/require-param-description': 'warn', + //TypeScript and IntelliSense already provides us information about the function typings while hovering and + // eslint-jsdoc doesn't detect a mismatch between what's declared in the function and what's declared in + // JSDOC. + 'jsdoc/require-param-type': 'off', + 'jsdoc/require-returns-type': 'off', + 'jsdoc/valid-types': 'off', + 'padding-line-between-statements': [ + 'error', + // Always require blank lines after directives (like 'use-strict'), except between directives + { blankLine: 'always', next: '*', prev: 'directive' }, + { blankLine: 'any', next: 'directive', prev: 'directive' }, + // Always require blank lines after import, except between imports + { blankLine: 'always', next: '*', prev: 'import' }, + { blankLine: 'any', next: 'import', prev: 'import' }, + // Always require blank lines before and after every sequence of variable declarations and export + { + blankLine: 'always', + next: ['const', 'let', 'var', 'export'], + prev: '*' + }, + { + blankLine: 'always', + next: '*', + prev: ['const', 'let', 'var', 'export'] + }, + { + blankLine: 'any', + next: ['const', 'let', 'var', 'export'], + prev: ['const', 'let', 'var', 'export'] + }, + // Always require blank lines before and after class declaration, if, do/while, switch, try + { + blankLine: 'always', + next: ['if', 'class', 'for', 'do', 'while', 'switch', 'try'], + prev: '*' + }, + { + blankLine: 'always', + next: '*', + prev: ['if', 'class', 'for', 'do', 'while', 'switch', 'try'] + }, + // Always require blank lines before return statements + { blankLine: 'always', next: 'return', prev: '*' } + ], + 'prefer-arrow-callback': 'error', + 'prefer-template': 'error', + 'promise/no-nesting': 'error', + 'promise/no-return-in-finally': 'error', + 'promise/prefer-await-to-callbacks': 'error', + 'promise/prefer-await-to-then': 'error', + 'sort-keys': [ + 'error', + 'asc', + { caseSensitive: false, minKeys: 2, natural: true } + ], + 'sort-vars': 'error' + }, settings: { 'import/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'] diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f05e2b74..c9020f46 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,30 +1,30 @@ version: 2 updates: -# Fetch and update latest `npm` packages -- package-ecosystem: npm - directory: '/' - schedule: - interval: daily - time: '00:00' - open-pull-requests-limit: 10 - reviewers: - - YouKnowBlom - - ferferga - - hawken93 - assignees: - - YouKnowBlom - - ferferga - - hawken93 - commit-message: - prefix: fix - prefix-development: chore - include: scope + # Fetch and update latest `npm` packages + - package-ecosystem: npm + directory: '/' + schedule: + interval: daily + time: '00:00' + open-pull-requests-limit: 10 + reviewers: + - YouKnowBlom + - ferferga + - hawken93 + assignees: + - YouKnowBlom + - ferferga + - hawken93 + commit-message: + prefix: fix + prefix-development: chore + include: scope -- package-ecosystem: github-actions - directory: '/' - schedule: - interval: daily - time: '00:00' - open-pull-requests-limit: 10 - commit-message: - prefix: actions + - package-ecosystem: github-actions + directory: '/' + schedule: + interval: daily + time: '00:00' + open-pull-requests-limit: 10 + commit-message: + prefix: actions diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 7c593b94..c6e6d03b 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -1,42 +1,42 @@ name: Lint on: - push: - branches: - - master - pull_request: - branches: - - master + push: + branches: + - master + pull_request: + branches: + - master jobs: - lint: - name: Lint TS and CSS - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v2.3.4 - - - name: Setup node env - uses: actions/setup-node@v2.1.5 - with: - node-version: 14 - - - name: Get npm cache directory path - id: npm-cache-dir-path - run: echo "::set-output name=dir::$(npm config get cache)" - - - name: Cache node_modules - uses: actions/cache@v2.1.5 - id: npm-cache - with: - path: ${{ steps.npm-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-npm- - - - name: Install dependencies - run: npm ci --no-audit - - - name: Run ESLint - run: npm run lint + lint: + name: Lint TS and CSS + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Setup node env + uses: actions/setup-node@v2.1.5 + with: + node-version: 14 + + - name: Get npm cache directory path + id: npm-cache-dir-path + run: echo "::set-output name=dir::$(npm config get cache)" + + - name: Cache node_modules + uses: actions/cache@v2.1.5 + id: npm-cache + with: + path: ${{ steps.npm-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm- + + - name: Install dependencies + run: npm ci --no-audit + + - name: Run ESLint + run: npm run lint diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 47506eee..bbf977ab 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,42 +1,42 @@ name: Test on: - push: - branches: - - master - pull_request: - branches: - - master + push: + branches: + - master + pull_request: + branches: + - master jobs: - jest: - name: Jest - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v2.3.4 - - - name: Setup node env - uses: actions/setup-node@v2.1.5 - with: - node-version: 14 - - - name: Get npm cache directory path - id: npm-cache-dir-path - run: echo "::set-output name=dir::$(npm config get cache)" - - - name: Cache node_modules - uses: actions/cache@v2.1.5 - id: npm-cache - with: - path: ${{ steps.npm-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-npm- - - - name: Install dependencies - run: npm ci --no-audit - - - name: Run tests - run: npm run test + jest: + name: Jest + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2.3.4 + + - name: Setup node env + uses: actions/setup-node@v2.1.5 + with: + node-version: 14 + + - name: Get npm cache directory path + id: npm-cache-dir-path + run: echo "::set-output name=dir::$(npm config get cache)" + + - name: Cache node_modules + uses: actions/cache@v2.1.5 + id: npm-cache + with: + path: ${{ steps.npm-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm- + + - name: Install dependencies + run: npm ci --no-audit + + - name: Run tests + run: npm run test diff --git a/.gitignore b/.gitignore index 08a14460..c4bb4e85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # ide .idea -.vscode tags # npm/yarn diff --git a/.prettierignore b/.prettierignore index 05c049de..7e8856fc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,6 @@ dist/ node_modules/ src/api/generated/ +package-lock.json LICENSE.md diff --git a/.prettierrc b/.prettierrc index 3b774b71..d65b3153 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,6 @@ { - "singleQuote": true, - "trailingComma": "none", "semi": true, - "tabWidth": 4 + "singleQuote": true, + "tabWidth": 4, + "trailingComma": "none" } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..f89ed5f1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.formatOnSave": true +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cef39b00..465e9c98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,8 +12,8 @@ The development environment is setup with editorconfig. Code style is enforced b ### Environment variables -| name | required | description | default if not set | -| ------------- | -------- | ---------------------------------------------------------- | ------------------ | +| name | required | description | default if not set | +| ------------- | -------- | --------------------------------------------------------- | ------------------ | | RECEIVER_PORT | No | The port used for the dev server when `npm start` is used | 9000 | ### Building/Using diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 00000000..a989bfc0 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@commitlint/config-conventional'] +}; diff --git a/package-lock.json b/package-lock.json index 9268ac04..f8923969 100644 --- a/package-lock.json +++ b/package-lock.json @@ -488,6 +488,302 @@ "minimist": "^1.2.0" } }, + "@commitlint/cli": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-12.1.1.tgz", + "integrity": "sha512-SB67/s6VJ50seoPx/Sr2gj1fMzKrx+udgarecGdr8h43ah+M2e22gjQJ7xHv5KwyPQ+6ug1YOMCL34ubT4zupQ==", + "dev": true, + "requires": { + "@commitlint/format": "^12.1.1", + "@commitlint/lint": "^12.1.1", + "@commitlint/load": "^12.1.1", + "@commitlint/read": "^12.1.1", + "@commitlint/types": "^12.1.1", + "get-stdin": "8.0.0", + "lodash": "^4.17.19", + "resolve-from": "5.0.0", + "resolve-global": "1.0.0", + "yargs": "^16.2.0" + }, + "dependencies": { + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "dev": true + } + } + }, + "@commitlint/config-conventional": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-12.1.1.tgz", + "integrity": "sha512-15CqbXMsQiEb0qbzjEHe2OkzaXPYSp7RxaS6KoSVk/4W0QiigquavQ+M0huBZze92h0lMS6Pxoq4AJ5CQ3D+iQ==", + "dev": true, + "requires": { + "conventional-changelog-conventionalcommits": "^4.3.1" + } + }, + "@commitlint/ensure": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-12.1.1.tgz", + "integrity": "sha512-XEUQvUjzBVQM7Uv8vYz+c7PDukFvx0AvQEyX/V+PaTkCK/xPvexu7FLbFwvypjSt9BPMf+T/rhB1hVmldkd6lw==", + "dev": true, + "requires": { + "@commitlint/types": "^12.1.1", + "lodash": "^4.17.19" + } + }, + "@commitlint/execute-rule": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-12.1.1.tgz", + "integrity": "sha512-6mplMGvLCKF5LieL7BRhydpg32tm6LICnWQADrWU4S5g9PKi2utNvhiaiuNPoHUXr29RdbNaGNcyyPv8DSjJsQ==", + "dev": true + }, + "@commitlint/format": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-12.1.1.tgz", + "integrity": "sha512-bTAoOryTFLqls17JTaRwk2WDVOP0NwuG4F/JPK8RaF6DMZNVQTfajkgTxFENNZRnESfau1BvivvEXfUAW2ZsvA==", + "dev": true, + "requires": { + "@commitlint/types": "^12.1.1", + "chalk": "^4.0.0" + } + }, + "@commitlint/is-ignored": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-12.1.1.tgz", + "integrity": "sha512-Sn4fsnWX+wLAJOD/UZeoVruB98te1TyPYRiDEq0MhRJAQIrP+7jE/O3/ass68AAMq00HvH3OK9kt4UBXggcGjA==", + "dev": true, + "requires": { + "@commitlint/types": "^12.1.1", + "semver": "7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@commitlint/lint": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-12.1.1.tgz", + "integrity": "sha512-FFFPpku/E0svL1jaUVqosuZJDDWiNWYBlUw5ZEljh3MwWRcoaWtMIX5bseX+IvHpFZsCTAiBs1kCgNulCi0UvA==", + "dev": true, + "requires": { + "@commitlint/is-ignored": "^12.1.1", + "@commitlint/parse": "^12.1.1", + "@commitlint/rules": "^12.1.1", + "@commitlint/types": "^12.1.1" + } + }, + "@commitlint/load": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-12.1.1.tgz", + "integrity": "sha512-qOQtgNdJRULUQWP9jkpTwhj7aEtnqUtqeUpbQ9rjS+GIUST65HZbteNUX4S0mAEGPWqy2aK5xGd73cUfFSvuuw==", + "dev": true, + "requires": { + "@commitlint/execute-rule": "^12.1.1", + "@commitlint/resolve-extends": "^12.1.1", + "@commitlint/types": "^12.1.1", + "chalk": "^4.0.0", + "cosmiconfig": "^7.0.0", + "lodash": "^4.17.19", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@commitlint/message": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-12.1.1.tgz", + "integrity": "sha512-RakDSLAiOligXjhbLahV8HowF4K75pZIcs0+Ii9Q8Gz5H3DWf1Ngit7alFTWfcbf/+DTjSzVPov5HiwQZPIBUg==", + "dev": true + }, + "@commitlint/parse": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-12.1.1.tgz", + "integrity": "sha512-nuljIvAbBDr93DgL0wCArftEIhjSghawAwhvrKNV9FFcqAJqfVqitwMxJrNDCQ5pgUMCSKULLOEv+dA0bLlTEQ==", + "dev": true, + "requires": { + "@commitlint/types": "^12.1.1", + "conventional-changelog-angular": "^5.0.11", + "conventional-commits-parser": "^3.0.0" + } + }, + "@commitlint/read": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-12.1.1.tgz", + "integrity": "sha512-1k0CQEoZIdixvmqZRKEcWdj2XiKS7SlizEOJ1SE99Qui5d5FlBey8eaooTGgmpR6zObpIHJehtEPzM3VzUT3qA==", + "dev": true, + "requires": { + "@commitlint/top-level": "^12.1.1", + "@commitlint/types": "^12.1.1", + "fs-extra": "^9.0.0", + "git-raw-commits": "^2.0.0" + } + }, + "@commitlint/resolve-extends": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-12.1.1.tgz", + "integrity": "sha512-/DXRt0S0U3o9lq5cc8OL1Lkx0IjW0HcDWjUkUXshAajBIKBYSJB8x/loNCi1krNEJ8SwLXUEFt5OLxNO6wE9yQ==", + "dev": true, + "requires": { + "import-fresh": "^3.0.0", + "lodash": "^4.17.19", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@commitlint/rules": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-12.1.1.tgz", + "integrity": "sha512-oCcLF/ykcJfhM2DeeaDyrgdaiuKsqIPNocugdPj2WEyhSYqmx1/u18CV96LAtW+WyyiOLCCeiZwiQutx3T5nXg==", + "dev": true, + "requires": { + "@commitlint/ensure": "^12.1.1", + "@commitlint/message": "^12.1.1", + "@commitlint/to-lines": "^12.1.1", + "@commitlint/types": "^12.1.1" + } + }, + "@commitlint/to-lines": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-12.1.1.tgz", + "integrity": "sha512-W23AH2XF5rI27MOAPSSr0TUDoRe7ZbFoRtYhFnPu2MBmcuDA9Tmfd9N5sM2tBXtdE26uq3SazwKqGt1OoGAilQ==", + "dev": true + }, + "@commitlint/top-level": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-12.1.1.tgz", + "integrity": "sha512-g7uRbr81QEIg+pbii0OkE17Zh/2C/f6dSmiMDVRn1S0+hNHR1bENCh18hVUKcV/qKTUsKkFlhhWXM9mQBfxQJw==", + "dev": true, + "requires": { + "find-up": "^5.0.0" + }, + "dependencies": { + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "@commitlint/types": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-12.1.1.tgz", + "integrity": "sha512-+qGH+s2Lo6qwacV2X3/ZypZwaAI84ift+1HBjXdXtI/q0F5NtmXucV3lcQOTviMTNiJhq4qWON2fjci2NItASw==", + "dev": true, + "requires": { + "chalk": "^4.0.0" + } + }, "@discoveryjs/json-ext": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz", @@ -1623,6 +1919,16 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, "abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -1778,18 +2084,18 @@ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", "dev": true }, - "array-differ": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", - "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", - "dev": true - }, "array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", "dev": true }, + "array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", + "dev": true + }, "array-includes": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz", @@ -1832,12 +2138,6 @@ "es-abstract": "^1.18.0-next.1" } }, - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "dev": true - }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -1892,6 +2192,12 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -2340,6 +2646,12 @@ "unset-value": "^1.0.0" } }, + "cachedir": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.2.0.tgz", + "integrity": "sha512-VvxA0xhNqIIfg0V9AmJkDg91DaJwryutH5rVEZAhcNi4iJFj9f+QxmAjgK1LT9I8OgToX27fypX6/MeCXVbBjQ==", + "dev": true + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -2454,6 +2766,12 @@ "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", "dev": true }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", @@ -2708,6 +3026,21 @@ } } }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, "cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -2866,63 +3199,208 @@ "integrity": "sha512-AOdq0i8ghZudnYv8RUnHrhTgafUGs61Rdz9jemU5x2lnZwAWyOq7vySo626K59e1fVKH1xSRorJwPVRLSWOoAQ==", "dev": true }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "requires": { - "mime-db": ">= 1.43.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" + "commitizen": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.2.3.tgz", + "integrity": "sha512-pYlYEng7XMV2TW4xtjDKBGqeJ0Teq2zyRSx2S3Ml1XAplHSlJZK8vm1KdGclpMEZuGafbS5TeHXIVnHk8RWIzQ==", + "dev": true, + "requires": { + "cachedir": "2.2.0", + "cz-conventional-changelog": "3.2.0", + "dedent": "0.7.0", + "detect-indent": "6.0.0", + "find-node-modules": "2.0.0", + "find-root": "1.1.0", + "fs-extra": "8.1.0", + "glob": "7.1.4", + "inquirer": "6.5.2", + "is-utf8": "^0.2.1", + "lodash": "^4.17.20", + "minimist": "1.2.5", + "strip-bom": "4.0.0", + "strip-json-comments": "3.0.1" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "ms": "2.0.0" + "color-convert": "^1.9.0" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "cz-conventional-changelog": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.2.0.tgz", + "integrity": "sha512-yAYxeGpVi27hqIilG1nh4A9Bnx4J3Ov+eXy4koL3drrR+IO9GaWPsKjik20ht608Asqi8TQPf0mczhEeyAtMzg==", + "dev": true, + "requires": { + "@commitlint/load": ">6.1.1", + "chalk": "^2.4.1", + "commitizen": "^4.0.3", + "conventional-commit-types": "^3.0.0", + "lodash.map": "^4.5.1", + "longest": "^2.0.1", + "word-wrap": "^1.0.3" + } + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "requires": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, "connect-history-api-fallback": { @@ -2954,12 +3432,232 @@ } } }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true - }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "conventional-changelog-angular": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.12.tgz", + "integrity": "sha512-5GLsbnkR/7A89RyHLvvoExbiGbd9xKdKqDTrArnPbOqBqG/2wIosu0fHwpeIRI8Tl94MhVNBXcLJZl92ZQ5USw==", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "q": "^1.5.1" + } + }, + "conventional-changelog-conventionalcommits": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.5.0.tgz", + "integrity": "sha512-buge9xDvjjOxJlyxUnar/+6i/aVEVGA7EEh4OafBCXPlLUQPGbRUBhBUveWRxzvR8TEjhKEP4BdepnpG2FSZXw==", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + } + }, + "conventional-commit-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-3.0.0.tgz", + "integrity": "sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==", + "dev": true + }, + "conventional-commits-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.1.tgz", + "integrity": "sha512-OG9kQtmMZBJD/32NEw5IhN5+HnBqVjy03eC+I71I0oQRFA5rOgA4OtPOYG7mz1GkCfCNxn3gKIX8EiHJYuf1cA==", + "dev": true, + "requires": { + "JSONStream": "^1.0.4", + "is-text-path": "^1.0.1", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0", + "trim-off-newlines": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "hosted-git-info": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", + "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + } + }, + "normalize-package-data": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.2.tgz", + "integrity": "sha512-6CdZocmfGaKnIHPVFhJJZ3GuR8SsLKvDANFp47Jmy51aKIr8akjAWTSxtpI+MBgBFdSMRyo4hMpDlT6dTffgZg==", + "dev": true, + "requires": { + "hosted-git-info": "^4.0.1", + "resolve": "^1.20.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true + }, + "yargs-parser": { + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "dev": true + } + } + }, "convert-source-map": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", @@ -3200,6 +3898,79 @@ } } }, + "cz-conventional-changelog": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz", + "integrity": "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==", + "dev": true, + "requires": { + "@commitlint/load": ">6.1.1", + "chalk": "^2.4.1", + "commitizen": "^4.0.3", + "conventional-commit-types": "^3.0.0", + "lodash.map": "^4.5.1", + "longest": "^2.0.1", + "word-wrap": "^1.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "dargs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", + "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", + "dev": true + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -3265,6 +4036,12 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, "deep-equal": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", @@ -3416,6 +4193,18 @@ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", "dev": true }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "detect-indent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz", + "integrity": "sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==", + "dev": true + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3527,6 +4316,15 @@ } } }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -3940,6 +4738,16 @@ "spdx-expression-parse": "^3.0.1" } }, + "eslint-plugin-json": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-json/-/eslint-plugin-json-2.1.2.tgz", + "integrity": "sha512-isM/fsUxS4wN1+nLsWoV5T4gLgBQnsql3nMTr8u+cEls1bL8rRQO5CP5GtxJxaOfbcKqnz401styw+H/P+e78Q==", + "dev": true, + "requires": { + "lodash": "^4.17.19", + "vscode-json-languageservice": "^3.7.0" + } + }, "eslint-plugin-prettier": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz", @@ -4223,6 +5031,15 @@ } } }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, "expect": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", @@ -4337,6 +5154,17 @@ } } }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -4485,6 +5313,15 @@ "bser": "2.1.1" } }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4558,6 +5395,22 @@ } } }, + "find-node-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-node-modules/-/find-node-modules-2.0.0.tgz", + "integrity": "sha512-8MWIBRgJi/WpjjfVXumjPKCtmQ10B+fjx6zmSA+770GMJirLhWIzg8l763rhjl9xaeaHbnxPNRQKq2mgMhr+aw==", + "dev": true, + "requires": { + "findup-sync": "^3.0.0", + "merge": "^1.2.1" + } + }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -4567,6 +5420,123 @@ "locate-path": "^2.0.0" } }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, "flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -4643,6 +5613,26 @@ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", "dev": true }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "dependencies": { + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4727,6 +5717,197 @@ "assert-plus": "^1.0.0" } }, + "git-raw-commits": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.10.tgz", + "integrity": "sha512-sHhX5lsbG9SOO6yXdlwgEMQ/ljIn7qMpAbJZCGfXX2fq5T8M5SrDnpYk9/4HswTildcIqatsWa91vty6VhWSaQ==", + "dev": true, + "requires": { + "dargs": "^7.0.0", + "lodash": "^4.17.15", + "meow": "^8.0.0", + "split2": "^3.0.0", + "through2": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "hosted-git-info": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", + "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + } + }, + "normalize-package-data": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.2.tgz", + "integrity": "sha512-6CdZocmfGaKnIHPVFhJJZ3GuR8SsLKvDANFp47Jmy51aKIr8akjAWTSxtpI+MBgBFdSMRyo4hMpDlT6dTffgZg==", + "dev": true, + "requires": { + "hosted-git-info": "^4.0.1", + "resolve": "^1.20.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true + }, + "yargs-parser": { + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "dev": true + } + } + }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -4756,6 +5937,15 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, "global-modules": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", @@ -4959,6 +6149,15 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, "hosted-git-info": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", @@ -5384,6 +6583,135 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "internal-ip": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", @@ -5644,6 +6972,12 @@ "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", "dev": true }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, "is-path-cwd": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", @@ -5735,12 +7069,27 @@ "has-symbols": "^1.0.1" } }, + "is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", + "dev": true, + "requires": { + "text-extensions": "^1.0.0" + } + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -6538,6 +7887,36 @@ "minimist": "^1.2.5" } }, + "jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + }, + "dependencies": { + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + } + } + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -6661,6 +8040,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -6682,6 +8067,12 @@ "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==", "dev": true }, + "longest": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz", + "integrity": "sha1-eB4YMpaqlPbU2RbcM10NF676I/g=", + "dev": true + }, "longest-streak": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", @@ -7039,6 +8430,12 @@ } } }, + "merge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz", + "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", + "dev": true + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -7192,12 +8589,6 @@ "minimist": "^1.2.5" } }, - "mri": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.6.tgz", - "integrity": "sha512-oi1b3MfbyGa7FJMP9GmLTttni5JoICpYBRlq+x5V16fZbLsnL9N3wFqqIm/nIG43FjUFkFh9Epzp/kzUGUnJxQ==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7220,18 +8611,11 @@ "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", "dev": true }, - "multimatch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-4.0.0.tgz", - "integrity": "sha512-lDmx79y1z6i7RNx0ZGCPq1bzJ6ZoDDKbvh7jxr9SJcWLkShMzXrHbYVpTdnhNM5MXpDUxCQ4DgqVttVXlBgiBQ==", - "dev": true, - "requires": { - "@types/minimatch": "^3.0.3", - "array-differ": "^3.0.0", - "array-union": "^2.1.0", - "arrify": "^2.0.1", - "minimatch": "^3.0.4" - } + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true }, "nan": { "version": "2.14.2", @@ -7619,6 +9003,12 @@ "url-parse": "^1.4.3" } }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, "p-each-series": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", @@ -7726,6 +9116,12 @@ "error-ex": "^1.2.0" } }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, "parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -8427,122 +9823,6 @@ "react-is": "^17.0.1" } }, - "pretty-quick": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-3.1.0.tgz", - "integrity": "sha512-DtxIxksaUWCgPFN7E1ZZk4+Aav3CCuRdhrDSFZENb404sYMtuo9Zka823F+Mgeyt8Zt3bUiCjFzzWYE9LYqkmQ==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "execa": "^4.0.0", - "find-up": "^4.1.0", - "ignore": "^5.1.4", - "mri": "^1.1.5", - "multimatch": "^4.0.0" - }, - "dependencies": { - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -9213,18 +10493,99 @@ } } }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "dependencies": { + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "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 }, + "resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "dev": true, + "requires": { + "global-dirs": "^0.1.1" + } + }, "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "dependencies": { + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + } + } + }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -9258,6 +10619,12 @@ "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", "dev": true }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9267,6 +10634,15 @@ "queue-microtask": "^1.2.2" } }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -10012,6 +11388,15 @@ "extend-shallow": "^3.0.0" } }, + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "requires": { + "readable-stream": "^3.0.0" + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -10673,6 +12058,12 @@ "minimatch": "^3.0.4" } }, + "text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -10685,12 +12076,36 @@ "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "dev": true }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "requires": { + "readable-stream": "3" + } + }, "thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", @@ -10776,6 +12191,12 @@ "integrity": "sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA==", "dev": true }, + "trim-off-newlines": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", + "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", + "dev": true + }, "trough": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", @@ -11257,6 +12678,43 @@ "unist-util-stringify-position": "^2.0.0" } }, + "vscode-json-languageservice": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-3.11.0.tgz", + "integrity": "sha512-QxI+qV97uD7HHOCjh3MrM1TfbdwmTXrMckri5Tus1/FQiG3baDZb2C9Y0y8QThs7PwHYBIQXcAc59ZveCRZKPA==", + "dev": true, + "requires": { + "jsonc-parser": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "3.16.0-next.2", + "vscode-nls": "^5.0.0", + "vscode-uri": "^2.1.2" + } + }, + "vscode-languageserver-textdocument": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz", + "integrity": "sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA==", + "dev": true + }, + "vscode-languageserver-types": { + "version": "3.16.0-next.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.2.tgz", + "integrity": "sha512-QjXB7CKIfFzKbiCJC4OWC8xUncLsxo19FzGVp/ADFvvi87PlmBSCAtZI5xwGjF5qE0xkLf0jjKUn3DzmpDP52Q==", + "dev": true + }, + "vscode-nls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.0.0.tgz", + "integrity": "sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA==", + "dev": true + }, + "vscode-uri": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.2.tgz", + "integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==", + "dev": true + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/package.json b/package.json index 51f76ca0..8698fec7 100644 --- a/package.json +++ b/package.json @@ -1,81 +1,84 @@ { - "name": "jellyfin-chromecast", - "description": "Cast receiver for Jellyfin", - "version": "3.0.0", - "bugs": { - "url": "https://github.com/jellyfin/jellyfin-chromecast/issues" - }, - "dependencies": { - "axios": "^0.21.1" - }, - "devDependencies": { - "@types/chromecast-caf-receiver": "^5.0.12", - "@types/jest": "^26.0.22", - "@types/node": "^14.14.39", - "@types/webpack": "^5.28.0", - "@types/webpack-dev-server": "^3.11.3", - "@types/webpack-merge": "^5.0.0", - "@typescript-eslint/eslint-plugin": "^4.22.0", - "@typescript-eslint/parser": "^4.22.0", - "clean-webpack-plugin": "^3.0.0", - "cross-env": "^7.0.3", - "css-loader": "^5.2.1", - "eslint": "^7.24.0", - "eslint-config-prettier": "^8.2.0", - "eslint-import-resolver-typescript": "^2.3.0", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-jsdoc": "^32.3.0", - "eslint-plugin-prettier": "^3.3.0", - "eslint-plugin-promise": "^5.1.0", - "file-loader": "^6.2.0", - "html-loader": "^2.1.2", - "html-webpack-plugin": "^5.3.1", - "husky": "^6.0.0", - "image-minimizer-webpack-plugin": "^2.1.0", - "imagemin-svgo": "^8.0.0", - "jest": "^26.6.3", - "prettier": "^2.2.1", - "pretty-quick": "^3.1.0", - "source-map-loader": "^2.0.1", - "style-loader": "^2.0.0", - "stylelint": "^13.8.0", - "stylelint-config-prettier": "^8.0.2", - "stylelint-config-standard": "^21.0.0", - "ts-jest": "^26.5.4", - "ts-loader": "^8.1.0", - "ts-node": "^9.1.1", - "tsconfig-paths": "^3.9.0", - "typescript": "^4.2.4", - "url-loader": "^4.1.1", - "webpack": "^5.33.2", - "webpack-cli": "^4.6.0", - "webpack-dev-server": "^3.11.0", - "webpack-merge": "^5.4.1" - }, - "engines": { - "yarn": "YARN NO LONGER USED - use npm instead." - }, - "homepage": "https://jellyfin.org/", - "husky": { - "hooks": { - "pre-commit": "pretty-quick --staged" + "name": "jellyfin-chromecast", + "description": "Cast receiver for Jellyfin", + "version": "3.0.0", + "bugs": { + "url": "https://github.com/jellyfin/jellyfin-chromecast/issues" + }, + "dependencies": { + "axios": "^0.21.1" + }, + "devDependencies": { + "@commitlint/cli": "^12.1.1", + "@commitlint/config-conventional": "^12.1.1", + "@types/chromecast-caf-receiver": "^5.0.12", + "@types/jest": "^26.0.22", + "@types/node": "^14.14.39", + "@types/webpack": "^5.28.0", + "@types/webpack-dev-server": "^3.11.3", + "@types/webpack-merge": "^5.0.0", + "@typescript-eslint/eslint-plugin": "^4.22.0", + "@typescript-eslint/parser": "^4.22.0", + "clean-webpack-plugin": "^3.0.0", + "cross-env": "^7.0.3", + "css-loader": "^5.2.1", + "cz-conventional-changelog": "^3.3.0", + "eslint": "^7.24.0", + "eslint-config-prettier": "^8.2.0", + "eslint-import-resolver-typescript": "^2.4.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsdoc": "^32.3.0", + "eslint-plugin-json": "^2.1.2", + "eslint-plugin-prettier": "^3.3.1", + "eslint-plugin-promise": "^5.1.0", + "file-loader": "^6.2.0", + "html-loader": "^2.1.2", + "html-webpack-plugin": "^5.3.1", + "husky": "^6.0.0", + "image-minimizer-webpack-plugin": "^2.1.0", + "imagemin-svgo": "^8.0.0", + "jest": "^26.6.3", + "prettier": "^2.2.1", + "source-map-loader": "^2.0.1", + "style-loader": "^2.0.0", + "stylelint": "^13.12.0", + "stylelint-config-prettier": "^8.0.2", + "stylelint-config-standard": "^21.0.0", + "ts-jest": "^26.5.4", + "ts-loader": "^8.1.0", + "ts-node": "^9.1.1", + "tsconfig-paths": "^3.9.0", + "typescript": "^4.2.4", + "url-loader": "^4.1.1", + "webpack": "^5.33.2", + "webpack-cli": "^4.6.0", + "webpack-dev-server": "^3.11.0", + "webpack-merge": "^5.4.1" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + }, + "engines": { + "yarn": "YARN NO LONGER USED - use npm instead." + }, + "homepage": "https://jellyfin.org/", + "license": "GPL-2.0-or-later", + "repository": { + "type": "git", + "url": "git+https://github.com/jellyfin/jellyfin-chromecast.git" + }, + "scripts": { + "build:development": "TS_NODE_PROJECT=\"tsconfig-webpack.json\" webpack --config webpack.config.ts --mode=development", + "build:production": "TS_NODE_PROJECT=\"tsconfig-webpack.json\" webpack --config webpack.config.ts --mode=production", + "lint": "npm run lint:code && npm run lint:css && npm run prettier", + "lint:code": "eslint --ext .ts,.js,.json .", + "lint:css": "stylelint **/*.css", + "prepare": "npm run build:production", + "prettier": "prettier --check .", + "start": "TS_NODE_PROJECT=\"tsconfig-webpack.json\" webpack serve --config webpack.config.ts", + "test": "jest --passWithNoTests", + "watch": "TS_NODE_PROJECT=\"tsconfig-webpack.json\" webpack --config webpack.config.ts --watch" } - }, - "license": "GPL-2.0-or-later", - "repository": { - "type": "git", - "url": "git+https://github.com/jellyfin/jellyfin-chromecast.git" - }, - "scripts": { - "build:development": "TS_NODE_PROJECT=\"tsconfig-webpack.json\" webpack --config webpack.config.ts --mode=development", - "build:production": "TS_NODE_PROJECT=\"tsconfig-webpack.json\" webpack --config webpack.config.ts --mode=production", - "lint": "npm run lint:code && npm run lint:css && npm run prettier", - "lint:code": "eslint --ext .ts,.js .", - "lint:css": "stylelint ./src/css/*.css", - "prepare": "npm run build:production && husky install", - "prettier": "prettier --check .", - "start": "TS_NODE_PROJECT=\"tsconfig-webpack.json\" webpack serve --config webpack.config.ts", - "test": "jest --passWithNoTests", - "watch": "TS_NODE_PROJECT=\"tsconfig-webpack.json\" webpack --config webpack.config.ts --watch" - } } diff --git a/src/api/credentialManager.ts b/src/api/credentialManager.ts index e4e49307..d2e59404 100644 --- a/src/api/credentialManager.ts +++ b/src/api/credentialManager.ts @@ -16,8 +16,7 @@ export class credentialManager { * Get credentials for the provided server ID * * @param serverId - ID of the server the credentials belong to - * @returns Credentials for the provided server ID - * or undefined if the store has no server with that ID + * @returns Credentials for the provided server ID or undefined if the store has no server with that ID */ get(serverId: string): Configuration | undefined { if (serverId in this.credentialStore) { @@ -35,6 +34,7 @@ export class credentialManager { update(serverId: string, newConfig: Configuration): boolean { if (serverId in this.credentialStore) { this.credentialStore[serverId] = newConfig; + return true; } @@ -54,6 +54,7 @@ export class credentialManager { } this.credentialStore[serverId] = configuration; + return true; } @@ -66,6 +67,7 @@ export class credentialManager { remove(serverId: string): boolean { if (serverId in this.credentialStore) { delete this.credentialStore[serverId]; + return true; } diff --git a/src/components/castDevices.ts b/src/components/castDevices.ts index 7da95f26..6c873847 100644 --- a/src/components/castDevices.ts +++ b/src/components/castDevices.ts @@ -20,7 +20,9 @@ let deviceId: number | null = null; * @returns Active Cast device Id. */ export function getActiveDeviceId(): number { - if (deviceId !== null) return deviceId; + if (deviceId !== null) { + return deviceId; + } if ( castContext.canDisplayType('video/mp4', 'hev1.1.6.L153.B0') && @@ -36,5 +38,6 @@ export function getActiveDeviceId(): number { } else { deviceId = deviceIds.AUDIO; } + return deviceId; } diff --git a/src/components/codecSupportHelper.ts b/src/components/codecSupportHelper.ts index 0318ae05..bc4ffb2f 100644 --- a/src/components/codecSupportHelper.ts +++ b/src/components/codecSupportHelper.ts @@ -62,7 +62,7 @@ export function hasH265Support(): boolean { * This is not supported on Chromecast Audio, * but otherwise is. * - * @param deviceId the device id + * @param deviceId - the device id * @returns true if text tracks are supported */ export function hasTextTrackSupport(deviceId: number): boolean { @@ -102,7 +102,7 @@ export function getMaxBitrateSupport(): number { /** * Get the max supported video width the active Cast device supports. * - * @param deviceId Cast device id. + * @param deviceId - Cast device id. * @returns Max supported width. */ export function getMaxWidthSupport(deviceId: number): number { @@ -123,8 +123,8 @@ export function getMaxWidthSupport(deviceId: number): number { /** * Get all H.26x profiles supported by the active Cast device. * - * @param {number} deviceId Cast device id. - * @returns {string} All supported H.26x profiles. + * @param deviceId - Cast device id. + * @returns All supported H.26x profiles. */ export function getH26xProfileSupport(deviceId: number): string { // These are supported by all Cast devices, excluding audio only devices. @@ -140,7 +140,7 @@ export function getH26xProfileSupport(deviceId: number): string { /** * Get the highest H.26x level supported by the active Cast device. * - * @param deviceId Cast device id. + * @param deviceId - Cast device id. * @returns The highest supported H.26x level. */ export function getH26xLevelSupport(deviceId: number): number { @@ -165,6 +165,7 @@ export function getH26xLevelSupport(deviceId: number): number { */ export function getSupportedVPXVideoCodecs(): Array { const codecs = []; + if (hasVP8Support()) { codecs.push('VP8'); } @@ -199,14 +200,18 @@ export function getSupportedMP4VideoCodecs(): Array { */ export function getSupportedMP4AudioCodecs(): Array { const codecs = []; + if (hasEAC3Support()) { codecs.push('eac3'); } + if (hasAC3Support()) { codecs.push('ac3'); } + codecs.push('aac'); codecs.push('mp3'); + return codecs; } diff --git a/src/components/commandHandler.ts b/src/components/commandHandler.ts index cf09065c..0e69dd1e 100644 --- a/src/components/commandHandler.ts +++ b/src/components/commandHandler.ts @@ -27,29 +27,29 @@ export abstract class CommandHandler { private static playerManager: framework.PlayerManager; private static playbackManager: playbackManager; private static supportedCommands: SupportedCommands = { - PlayNext: CommandHandler.playNextHandler, - PlayNow: CommandHandler.playNowHandler, - PlayLast: CommandHandler.playLastHandler, - Shuffle: CommandHandler.shuffleHandler, - InstantMix: CommandHandler.instantMixHandler, DisplayContent: CommandHandler.displayContentHandler, + Identify: CommandHandler.IdentifyHandler, + InstantMix: CommandHandler.instantMixHandler, + Mute: CommandHandler.MuteHandler, NextTrack: CommandHandler.nextTrackHandler, + Pause: CommandHandler.PauseHandler, + PlayLast: CommandHandler.playLastHandler, + PlayNext: CommandHandler.playNextHandler, + PlayNow: CommandHandler.playNowHandler, + PlayPause: CommandHandler.PlayPauseHandler, PreviousTrack: CommandHandler.previousTrackHandler, + Seek: CommandHandler.SeekHandler, SetAudioStreamIndex: CommandHandler.setAudioStreamIndexHandler, + SetRepeatMode: CommandHandler.SetRepeatModeHandler, SetSubtitleStreamIndex: CommandHandler.setSubtitleStreamIndexHandler, - VolumeUp: CommandHandler.VolumeUpHandler, - VolumeDown: CommandHandler.VolumeDownHandler, - ToggleMute: CommandHandler.ToggleMuteHandler, - Identify: CommandHandler.IdentifyHandler, SetVolume: CommandHandler.SetVolumeHandler, - Seek: CommandHandler.SeekHandler, - Mute: CommandHandler.MuteHandler, - Unmute: CommandHandler.MuteHandler, + Shuffle: CommandHandler.shuffleHandler, Stop: CommandHandler.StopHandler, - PlayPause: CommandHandler.PlayPauseHandler, - Pause: CommandHandler.PauseHandler, - SetRepeatMode: CommandHandler.SetRepeatModeHandler, - Unpause: CommandHandler.UnpauseHandler + ToggleMute: CommandHandler.ToggleMuteHandler, + Unmute: CommandHandler.MuteHandler, + Unpause: CommandHandler.UnpauseHandler, + VolumeDown: CommandHandler.VolumeDownHandler, + VolumeUp: CommandHandler.VolumeUpHandler }; static configure( @@ -201,6 +201,7 @@ export abstract class CommandHandler { static processMessage(data: DataMessage, command: string): void { const commandHandler = this.supportedCommands[command]; + if (typeof commandHandler === 'function') { console.debug( `Command "${command}" received. Identified handler, calling identified handler.` diff --git a/src/components/deviceprofileBuilder.ts b/src/components/deviceprofileBuilder.ts index 4b1087c1..7ba74082 100644 --- a/src/components/deviceprofileBuilder.ts +++ b/src/components/deviceprofileBuilder.ts @@ -41,11 +41,11 @@ let profileOptions: ProfileOptions; let currentDeviceId: number; /** - * @param {ProfileConditionValue} Property What property the condition should test. - * @param {ProfileConditionType} Condition The condition to test the values for. - * @param {string} Value The value to compare against. - * @param {boolean} [IsRequired=false] Don't permit unknown values - * @returns {ProfileCondition} A profile condition created from the parameters. + * @param Property - What property the condition should test. + * @param Condition - The condition to test the values for. + * @param Value - The value to compare against. + * @param [IsRequired=false] - Don't permit unknown values + * @returns A profile condition created from the parameters. */ function createProfileCondition( Property: ProfileConditionValue, @@ -55,9 +55,9 @@ function createProfileCondition( ): ProfileCondition { return { Condition, + IsRequired, Property, - Value, - IsRequired + Value }; } @@ -75,9 +75,9 @@ function getResponseProfiles(): Array { // This seems related to DLNA, it might not be needed? return [ { - Type: DlnaProfileType.Video, Container: 'm4v', - MimeType: 'video/mp4' + MimeType: 'video/mp4', + Type: DlnaProfileType.Video } ]; } @@ -96,18 +96,18 @@ function getDirectPlayProfiles(): Array { for (const codec of vpxVideoCodecs) { DirectPlayProfiles.push({ + AudioCodec: webmAudioCodecs.join(','), Container: 'webm', Type: DlnaProfileType.Video, - AudioCodec: webmAudioCodecs.join(','), VideoCodec: codec }); } DirectPlayProfiles.push({ + AudioCodec: mp4AudioCodecs.join(','), Container: 'mp4,m4v', Type: DlnaProfileType.Video, - VideoCodec: mp4VideoCodecs.join(','), - AudioCodec: mp4AudioCodecs.join(',') + VideoCodec: mp4VideoCodecs.join(',') }); } @@ -116,9 +116,9 @@ function getDirectPlayProfiles(): Array { for (const audioFormat of supportedAudio) { if (audioFormat === 'mp3') { DirectPlayProfiles.push({ + AudioCodec: audioFormat, Container: audioFormat, - Type: DlnaProfileType.Audio, - AudioCodec: audioFormat + Type: DlnaProfileType.Audio }); } else if (audioFormat === 'webma') { DirectPlayProfiles.push({ @@ -135,8 +135,8 @@ function getDirectPlayProfiles(): Array { // aac also appears in the m4a and m4b container if (audioFormat === 'aac') { DirectPlayProfiles.push({ - Container: 'm4a,m4b', AudioCodec: audioFormat, + Container: 'm4a,m4b', Type: DlnaProfileType.Audio }); } @@ -152,7 +152,6 @@ function getCodecProfiles(): Array { const CodecProfiles: Array = []; const audioConditions: CodecProfile = { - Type: CodecType.Audio, Codec: 'flac', Conditions: [ createProfileCondition( @@ -165,7 +164,8 @@ function getCodecProfiles(): Array { ProfileConditionType.LessThanEqual, '24' ) - ] + ], + Type: CodecType.Audio }; CodecProfiles.push(audioConditions); @@ -176,7 +176,6 @@ function getCodecProfiles(): Array { } const aacConditions: CodecProfile = { - Type: CodecType.VideoAudio, Codec: 'aac', Conditions: [ // Not sure what secondary audio means in this context. Multiple audio tracks? @@ -190,7 +189,8 @@ function getCodecProfiles(): Array { ProfileConditionType.LessThanEqual, '2' ) - ] + ], + Type: CodecType.VideoAudio }; CodecProfiles.push(aacConditions); @@ -200,7 +200,6 @@ function getCodecProfiles(): Array { const h26xProfile: string = getH26xProfileSupport(currentDeviceId); const h26xConditions: CodecProfile = { - Type: CodecType.Video, Codec: 'h264', Conditions: [ createProfileCondition( @@ -224,13 +223,13 @@ function getCodecProfiles(): Array { maxWidth.toString(), true ) - ] + ], + Type: CodecType.Video }; CodecProfiles.push(h26xConditions); const videoConditions: CodecProfile = { - Type: CodecType.Video, Conditions: [ createProfileCondition( ProfileConditionValue.Width, @@ -238,20 +237,21 @@ function getCodecProfiles(): Array { maxWidth.toString(), true ) - ] + ], + Type: CodecType.Video }; CodecProfiles.push(videoConditions); const videoAudioConditions: CodecProfile = { - Type: CodecType.VideoAudio, Conditions: [ createProfileCondition( ProfileConditionValue.IsSecondaryAudio, ProfileConditionType.Equals, 'false' ) - ] + ], + Type: CodecType.VideoAudio }; CodecProfiles.push(videoAudioConditions); @@ -270,14 +270,14 @@ function getTranscodingProfiles(): Array { if (profileOptions.enableHls !== false) { TranscodingProfiles.push({ - Container: 'ts', - Type: DlnaProfileType.Audio, AudioCodec: hlsAudioCodecs.join(','), + BreakOnNonKeyFrames: false, + Container: 'ts', Context: EncodingContext.Streaming, - Protocol: 'hls', MaxAudioChannels: audioChannels.toString(), MinSegments: 1, - BreakOnNonKeyFrames: false + Protocol: 'hls', + Type: DlnaProfileType.Audio }); } @@ -286,12 +286,12 @@ function getTranscodingProfiles(): Array { // audio only profiles here for (const audioFormat of supportedAudio) { TranscodingProfiles.push({ - Container: audioFormat, - Type: DlnaProfileType.Audio, AudioCodec: audioFormat, + Container: audioFormat, Context: EncodingContext.Streaming, + MaxAudioChannels: audioChannels.toString(), Protocol: 'http', - MaxAudioChannels: audioChannels.toString() + Type: DlnaProfileType.Audio }); } @@ -301,35 +301,36 @@ function getTranscodingProfiles(): Array { } const hlsVideoCodecs = getSupportedHLSVideoCodecs(); + if ( hlsVideoCodecs.length && hlsAudioCodecs.length && profileOptions.enableHls !== false ) { TranscodingProfiles.push({ - Container: 'ts', - Type: DlnaProfileType.Video, AudioCodec: hlsAudioCodecs.join(','), - VideoCodec: hlsVideoCodecs.join(','), + BreakOnNonKeyFrames: false, + Container: 'ts', Context: EncodingContext.Streaming, - Protocol: 'hls', MaxAudioChannels: audioChannels.toString(), MinSegments: 1, - BreakOnNonKeyFrames: false + Protocol: 'hls', + Type: DlnaProfileType.Video, + VideoCodec: hlsVideoCodecs.join(',') }); } if (hasVP8Support() || hasVP9Support()) { TranscodingProfiles.push({ - Container: 'webm', - Type: DlnaProfileType.Video, AudioCodec: 'vorbis', - VideoCodec: 'vpx', + Container: 'webm', Context: EncodingContext.Streaming, - Protocol: 'http', // If audio transcoding is needed, limit channels to number of physical audio channels // Trying to transcode to 5 channels when there are only 2 speakers generally does not sound good - MaxAudioChannels: audioChannels.toString() + MaxAudioChannels: audioChannels.toString(), + Protocol: 'http', + Type: DlnaProfileType.Video, + VideoCodec: 'vpx' }); } @@ -360,7 +361,7 @@ function getSubtitleProfiles(): Array { /** * Creates a device profile containing supported codecs for the active Cast device. * - * @param options Profile options + * @param options - Profile options * @returns Device profile. */ export function getDeviceProfile(options: ProfileOptions): DeviceProfile { @@ -369,8 +370,8 @@ export function getDeviceProfile(options: ProfileOptions): DeviceProfile { // MaxStaticBitrate seems to be for offline sync only const profile: DeviceProfile = { - MaxStreamingBitrate: options.bitrateSetting, MaxStaticBitrate: options.bitrateSetting, + MaxStreamingBitrate: options.bitrateSetting, MusicStreamingTranscodingBitrate: Math.min( options.bitrateSetting, 192000 diff --git a/src/components/documentManager.ts b/src/components/documentManager.ts index b0476096..a4f975bb 100644 --- a/src/components/documentManager.ts +++ b/src/components/documentManager.ts @@ -16,16 +16,17 @@ export abstract class DocumentManager { * Hide the document body on chromecast audio to save resources */ public static initialize(): void { - if (getActiveDeviceId() === deviceIds.AUDIO) + if (getActiveDeviceId() === deviceIds.AUDIO) { document.body.style.display = 'none'; + } } /** * Set the background image for a html element, without preload. * You should do the preloading first with preloadImage. * - * @param {HTMLElement} element HTML Element - * @param {string | null} src URL to the image or null to remove the active one + * @param element - HTML Element + * @param src - URL to the image or null to remove the active one */ private static setBackgroundImage( element: HTMLElement, @@ -41,13 +42,14 @@ export abstract class DocumentManager { /** * Preload an image * - * @param {string | null} src URL to the image or null - * @returns {Promise} wait for the preload and return the url to use. Might be nulled after loading error. + * @param src - URL to the image or null + * @returns wait for the preload and return the url to use. Might be nulled after loading error. */ private static preloadImage(src: string | null): Promise { if (src) { return new Promise((resolve, reject) => { const preload = new Image(); + preload.src = src; preload.addEventListener('load', () => { resolve(src); @@ -65,8 +67,8 @@ export abstract class DocumentManager { /** * Get url for primary image for a given item * - * @param {BaseItemDto} item to look up - * @returns {Promise} url to image after preload + * @param item - to look up + * @returns url to image after preload */ private static getPrimaryImageUrl( item: BaseItemDto @@ -102,11 +104,12 @@ export abstract class DocumentManager { /** * Get url for logo image for a given item * - * @param {BaseItemDto} item to look up - * @returns {Promise} url to logo image after preload + * @param item - to look up + * @returns url to logo image after preload */ private static getLogoUrl(item: BaseItemDto): Promise { let src: string | null = null; + if (item.ImageTags?.Logo && item.Id) { src = JellyfinApi.createImageUrl( item.Id, @@ -129,65 +132,67 @@ export abstract class DocumentManager { * on the details page. This happens when no media is playing, * and the connected client is browsing the library. * - * @param {BaseItemDto} item to show information about - * @returns {Promise} for the page to load + * @param item - to show information about + * @returns for the page to load */ - public static showItem(item: BaseItemDto): Promise { + public static async showItem(item: BaseItemDto): Promise { // no showItem for cc audio if (getActiveDeviceId() === deviceIds.AUDIO) { - return Promise.resolve(); + return; } // stop cycling backdrops this.clearBackdropInterval(); - return Promise.all([ + const promises = [ this.getWaitingBackdropUrl(item), this.getPrimaryImageUrl(item), this.getLogoUrl(item) - ]).then((urls) => { - requestAnimationFrame(() => { - this.setWaitingBackdrop(urls[0], item); - this.setDetailImage(urls[1]); - this.setLogo(urls[2]); - - this.setOverview(item.Overview ?? null); - this.setGenres(item?.Genres?.join(' / ') ?? null); - this.setDisplayName(item); - this.setMiscInfo(item); - - this.setRating(item); - - if (item?.UserData?.Played) { - this.setPlayedIndicator(true); - } else if (item?.UserData?.UnplayedItemCount) { - this.setPlayedIndicator(item?.UserData?.UnplayedItemCount); - } else { - this.setPlayedIndicator(false); - } + ]; - if ( - item?.UserData?.PlayedPercentage && - item?.UserData?.PlayedPercentage < 100 && - !item.IsFolder - ) { - this.setHasPlayedPercentage(false); - this.setPlayedPercentage(item.UserData.PlayedPercentage); - } else { - this.setHasPlayedPercentage(false); - this.setPlayedPercentage(0); - } + const urls = await Promise.all(promises); - // Switch visible view! - this.setAppStatus('details'); - }); + requestAnimationFrame(() => { + this.setWaitingBackdrop(urls[0], item); + this.setDetailImage(urls[1]); + this.setLogo(urls[2]); + + this.setOverview(item.Overview ?? null); + this.setGenres(item?.Genres?.join(' / ') ?? null); + this.setDisplayName(item); + this.setMiscInfo(item); + + this.setRating(item); + + if (item?.UserData?.Played) { + this.setPlayedIndicator(true); + } else if (item?.UserData?.UnplayedItemCount) { + this.setPlayedIndicator(item?.UserData?.UnplayedItemCount); + } else { + this.setPlayedIndicator(false); + } + + if ( + item?.UserData?.PlayedPercentage && + item?.UserData?.PlayedPercentage < 100 && + !item.IsFolder + ) { + this.setHasPlayedPercentage(false); + this.setPlayedPercentage(item.UserData.PlayedPercentage); + } else { + this.setHasPlayedPercentage(false); + this.setPlayedPercentage(0); + } + + // Switch visible view! + this.setAppStatus('details'); }); } /** * Set value of played indicator * - * @param {boolean | number} value True = played, false = not visible, number = number of unplayed items + * @param value - True = played, false = not visible, number = number of unplayed items */ private static setPlayedIndicator(value: boolean | number): void { const playedIndicatorOk = this.getElementById('played-indicator-ok'); @@ -215,15 +220,17 @@ export abstract class DocumentManager { * Show item, but from just the id number, not an actual item. * Looks up the item and then calls showItem * - * @param {string} itemId id of item to look up - * @returns {Promise} promise that resolves when the item is shown + * @param itemId - id of item to look up + * @returns promise that resolves when the item is shown */ public static async showItemId(itemId: string): Promise { // no showItemId for cc audio - if (getActiveDeviceId() === deviceIds.AUDIO) return; + if (getActiveDeviceId() === deviceIds.AUDIO) { + return; + } const item: BaseItemDto = await JellyfinApi.authAjaxUser( - 'Items/' + itemId, + `Items/${itemId}`, { dataType: 'json', type: 'GET' @@ -236,7 +243,7 @@ export abstract class DocumentManager { /** * Update item rating elements * - * @param {BaseItemDto} item to look up + * @param item - to look up */ private static setRating(item: BaseItemDto): void { const starRating = this.getElementById('star-rating'); @@ -276,7 +283,7 @@ export abstract class DocumentManager { * Set the status of the app, and switch the visible view * to the corresponding one. * - * @param {string} status to set + * @param status - to set */ public static setAppStatus(status: string): void { this.status = status; @@ -286,7 +293,7 @@ export abstract class DocumentManager { /** * Get the status of the app * - * @returns {string} app status + * @returns app status */ public static getAppStatus(): string { return this.status; @@ -297,8 +304,8 @@ export abstract class DocumentManager { /** * Get url to the backdrop image, and return a preload promise. * - * @param {BaseItemDto | null} item Item to use for waiting backdrop, null to remove it. - * @returns {Promise} promise for the preload to complete + * @param item - Item to use for waiting backdrop, null to remove it. + * @returns promise for the preload to complete */ public static getWaitingBackdropUrl( item: BaseItemDto | null @@ -340,8 +347,8 @@ export abstract class DocumentManager { * They are switched around every 30 seconds by default * (governed by startBackdropInterval) * - * @param {string | null} src Url to image - * @param {BaseItemDto | null} item Item to use for waiting backdrop, null to remove it. + * @param src - Url to image + * @param item - Item to use for waiting backdrop, null to remove it. */ public static async setWaitingBackdrop( src: string | null, @@ -360,22 +367,22 @@ export abstract class DocumentManager { /** * Set a random backdrop on the waiting container * - * @returns {Promise} promise waiting for the backdrop to be set + * @returns promise waiting for the backdrop to be set */ private static async setRandomUserBackdrop(): Promise { const result = await JellyfinApi.authAjaxUser('Items', { dataType: 'json', - type: 'GET', query: { - SortBy: 'Random', - IncludeItemTypes: 'Movie,Series', ImageTypes: 'Backdrop', - Recursive: true, + IncludeItemTypes: 'Movie,Series', Limit: 1, + MaxOfficialRating: 'PG-13', + Recursive: true, + SortBy: 'Random' // Although we're limiting to what the user has access to, // not everyone will want to see adult backdrops rotating on their TV. - MaxOfficialRating: 'PG-13' - } + }, + type: 'GET' }); let src: string | null = null; @@ -404,11 +411,13 @@ export abstract class DocumentManager { /** * Start the backdrop rotation, restart if running, stop if disabled * - * @returns {Promise} promise for the first backdrop to be set + * @returns promise for the first backdrop to be set */ public static async startBackdropInterval(): Promise { // no backdrop rotation for cc audio - if (getActiveDeviceId() === deviceIds.AUDIO) return; + if (getActiveDeviceId() === deviceIds.AUDIO) { + return; + } // avoid running it multiple times this.clearBackdropInterval(); @@ -416,6 +425,7 @@ export abstract class DocumentManager { // skip out if it's disabled if (!this.backdropPeriodMs) { this.setWaitingBackdrop(null, null); + return; } @@ -432,7 +442,7 @@ export abstract class DocumentManager { /** * Set interval between backdrop changes, null to disable * - * @param {number | null} period in milliseconds or null + * @param period - in milliseconds or null */ public static setBackdropPeriodMs(period: number | null): void { if (period !== this.backdropPeriodMs) { @@ -456,11 +466,13 @@ export abstract class DocumentManager { * Set background behind the media player, * this is shown while the media is loading. * - * @param {BaseItemDto} item to get backdrop from + * @param item - to get backdrop from */ public static setPlayerBackdrop(item: BaseItemDto): void { // no backdrop rotation for cc audio - if (getActiveDeviceId() === deviceIds.AUDIO) return; + if (getActiveDeviceId() === deviceIds.AUDIO) { + return; + } let backdropUrl: string | null = null; @@ -500,10 +512,11 @@ export abstract class DocumentManager { /** * Set the URL to the item logo, or null to remove it * - * @param {string | null} src Source url or null + * @param src - Source url or null */ public static setLogo(src: string | null): void { const element: HTMLElement = this.querySelector('.detailLogo'); + this.setBackgroundImage(element, src); } @@ -511,10 +524,11 @@ export abstract class DocumentManager { * Set the URL to the item banner image (I think?), * or null to remove it * - * @param {string | null} src Source url or null + * @param src - Source url or null */ public static setDetailImage(src: string | null): void { const element: HTMLElement = this.querySelector('.detailImage'); + this.setBackgroundImage(element, src); } @@ -524,7 +538,7 @@ export abstract class DocumentManager { * This combines the old statement setDisplayName(getDisplayName(item)) * into setDisplayName(item). * - * @param {BaseItemDto} item source for the displayed name + * @param item - source for the displayed name */ private static setDisplayName(item: BaseItemDto): void { const name: string = item.EpisodeTitle ?? item.Name; @@ -532,7 +546,9 @@ export abstract class DocumentManager { let displayName: string = name; if (item.Type == 'TvChannel') { - if (item.Number) displayName = `${item.Number} ${name}`; + if (item.Number) { + displayName = `${item.Number} ${name}`; + } } else if ( item.Type == 'Episode' && item.IndexNumber != null && @@ -548,26 +564,29 @@ export abstract class DocumentManager { } const element = this.querySelector('.displayName'); + element.innerHTML = displayName || ''; } /** * Set the html of the genres container * - * @param {string | null} name String/html for genres box, null to empty + * @param name - String/html for genres box, null to empty */ private static setGenres(name: string | null): void { const element = this.querySelector('.genres'); + element.innerHTML = name || ''; } /** * Set the html of the overview container * - * @param {string | null} name string or html to insert + * @param name - string or html to insert */ private static setOverview(name: string | null): void { const element = this.querySelector('.overview'); + element.innerHTML = name || ''; } @@ -575,12 +594,13 @@ export abstract class DocumentManager { * Set the progress of the progress bar in the * item details page. (Not the same as the playback ui) * - * @param {number} value Percentage to set + * @param value - Percentage to set */ private static setPlayedPercentage(value = 0): void { const element = ( this.querySelector('.itemProgressBar') ); + element.value = value.toString(); } @@ -588,20 +608,24 @@ export abstract class DocumentManager { * Set the visibility of the item progress bar in the * item details page * - * @param {boolean} value If true, show progress on details page + * @param value - If true, show progress on details page */ private static setHasPlayedPercentage(value: boolean): void { const element = this.querySelector('.detailImageProgressContainer'); - if (value) (element).classList.remove('d-none'); - else (element).classList.add('d-none'); + + if (value) { + (element).classList.remove('d-none'); + } else { + (element).classList.add('d-none'); + } } /** * Get a human readable representation of the current position * in ticks * - * @param {number} ticks tick position - * @returns {string} human readable position + * @param ticks - tick position + * @returns human readable position */ private static formatRunningTime(ticks: number): string { const ticksPerHour = 36000000000; @@ -623,7 +647,7 @@ export abstract class DocumentManager { ticks -= minutes * ticksPerMinute; if (minutes < 10 && hours) { - parts.push('0' + minutes.toString()); + parts.push(`0${minutes.toString()}`); } else { parts.push(minutes.toString()); } @@ -631,7 +655,7 @@ export abstract class DocumentManager { const seconds: number = Math.floor(ticks / ticksPerSecond); if (seconds < 10) { - parts.push('0' + seconds.toString()); + parts.push(`0${seconds.toString()}`); } else { parts.push(seconds.toString()); } @@ -643,10 +667,11 @@ export abstract class DocumentManager { * Set information about mostly episodes or series * on the item details page * - * @param {BaseItemDto} item to look up + * @param item - to look up */ private static setMiscInfo(item: BaseItemDto): void { const info: Array = []; + if (item.Type == 'Episode') { if (item.PremiereDate) { try { @@ -654,41 +679,47 @@ export abstract class DocumentManager { parseISO8601Date(item.PremiereDate).toLocaleDateString() ); } catch (e) { - console.log('Error parsing date: ' + item.PremiereDate); + console.log(`Error parsing date: ${item.PremiereDate}`); } } } + if (item.StartDate) { try { info.push( parseISO8601Date(item.StartDate).toLocaleDateString() ); } catch (e) { - console.log('Error parsing date: ' + item.PremiereDate); + console.log(`Error parsing date: ${item.PremiereDate}`); } } + if (item.ProductionYear && item.Type == 'Series') { if (item.Status == 'Continuing') { info.push(`${item.ProductionYear}-Present`); } else if (item.ProductionYear) { let text: string = item.ProductionYear.toString(); + if (item.EndDate) { try { const endYear = parseISO8601Date( item.EndDate ).getFullYear(); + if (endYear != item.ProductionYear) { - text += - '-' + - parseISO8601Date(item.EndDate).getFullYear(); + text += `-${parseISO8601Date( + item.EndDate + ).getFullYear()}`; } } catch (e) { - console.log('Error parsing date: ' + item.EndDate); + console.log(`Error parsing date: ${item.EndDate}`); } } + info.push(text); } } + if (item.Type != 'Series' && item.Type != 'Episode') { if (item.ProductionYear) { info.push(item.ProductionYear.toString()); @@ -700,20 +731,23 @@ export abstract class DocumentManager { .toString() ); } catch (e) { - console.log('Error parsing date: ' + item.PremiereDate); + console.log(`Error parsing date: ${item.PremiereDate}`); } } } + let minutes; + if (item.RunTimeTicks && item.Type != 'Series') { if (item.Type == 'Audio') { info.push(this.formatRunningTime(item.RunTimeTicks)); } else { minutes = item.RunTimeTicks / 600000000; minutes = minutes || 1; - info.push(Math.round(minutes) + 'min'); + info.push(`${Math.round(minutes)}min`); } } + if ( item.OfficialRating && item.Type !== 'Season' && @@ -721,11 +755,13 @@ export abstract class DocumentManager { ) { info.push(item.OfficialRating); } + if (item.Video3DFormat) { info.push('3D'); } const element = this.getElementById('miscInfo'); + element.innerHTML = info.join('    '); } @@ -733,8 +769,8 @@ export abstract class DocumentManager { /** * Set the visibility of an element * - * @param {HTMLElement} element Element to set visibility on - * @param {boolean} visible True if the element should be visible. + * @param element - Element to set visibility on + * @param visible - True if the element should be visible. */ private static setVisibility(element: HTMLElement, visible: boolean): void { if (visible) { @@ -747,11 +783,12 @@ export abstract class DocumentManager { /** * Get a HTMLElement from id or throw an error * - * @param {string} id ID to look up - * @returns {HTMLElement} HTML Element + * @param id - ID to look up + * @returns HTML Element */ private static getElementById(id: string): HTMLElement { const element = document.getElementById(id); + if (!element) { throw new ReferenceError(`Cannot find element ${id} by id`); } @@ -762,11 +799,12 @@ export abstract class DocumentManager { /** * Get a HTMLElement by class * - * @param {string} cls Class to look up - * @returns {HTMLElement} HTML Element + * @param cls - Class to look up + * @returns HTML Element */ private static querySelector(cls: string): HTMLElement { const element: HTMLElement | null = document.querySelector(cls); + if (!element) { throw new ReferenceError(`Cannot find element ${cls} by class`); } diff --git a/src/components/fetchhelper.ts b/src/components/fetchhelper.ts index 90363090..0fe22826 100644 --- a/src/components/fetchhelper.ts +++ b/src/components/fetchhelper.ts @@ -1,18 +1,23 @@ /** * Function to send a request, with or without the timeout option * - * @param {any} request Custom request object, mostly modeled after RequestInit. - * @returns {Promise} response promise + * @param request - Custom request object, mostly modeled after RequestInit. + * @returns response promise */ function getFetchPromise(request: any): Promise { const headers = request.headers || {}; - if (request.dataType === 'json') headers.accept = 'application/json'; + + if (request.dataType === 'json') { + headers.accept = 'application/json'; + } + const fetchRequest: RequestInit = { + credentials: 'same-origin', headers: headers, - method: request.type, - credentials: 'same-origin' + method: request.type }; let contentType = request.contentType; + if (request.data) { if (typeof request.data == 'string') { fetchRequest.body = request.data; @@ -23,113 +28,116 @@ function getFetchPromise(request: any): Promise { 'application/x-www-form-urlencoded; charset=UTF-8'; } } + if (contentType) { headers['Content-Type'] = contentType; } + let url = request.url; + if (request.query) { const paramString = paramsToString(request.query); - paramString && (url += '?' + paramString); + + paramString && (url += `?${paramString}`); } + return request.timeout - ? fetchWithTimeout(url, fetchRequest, request.timeout) + ? fetchWithCredentials(url, fetchRequest) : fetch(url, fetchRequest); } /** * Timeout wrapper for fetch() * - * @param {string} url url to get - * @param {RequestInit} options RequestInit with additional options - * @param {number} timeoutMs request timeout in ms - * @returns {Promise} response promise + * @param url - url to get + * @param options - RequestInit with additional options + * @returns response promise */ -function fetchWithTimeout( +async function fetchWithCredentials( url: string, - options: RequestInit, - timeoutMs: number + options: RequestInit ): Promise { - console.log('fetchWithTimeout: timeoutMs: ' + timeoutMs + ', url: ' + url); - return new Promise(function (resolve, reject) { - const timeout = setTimeout(reject, timeoutMs); + console.log(`fetchWithCredentials: ${url}`); + + try { options = options || {}; options.credentials = 'same-origin'; - fetch(url, options).then( - function (response) { - clearTimeout(timeout); - console.log( - 'fetchWithTimeout: succeeded connecting to url: ' + url - ); - resolve(response); - }, - function () { - clearTimeout(timeout); - console.log( - 'fetchWithTimeout: timed out connecting to url: ' + url - ); - reject(); - } + + const response = await fetch(url, options); + + console.log( + `fetchWithCredentials: succeeded connecting to url: ${url}` ); - }); + + return response; + } catch (e) { + throw new Error( + `fetchWithCredentials: timed out connecting to url: ${url}` + ); + } } /** * Urlencode a dictionary of strings for use in POST form or GET requests * - * @param {Record} params Dictionary to encode - * @returns {string} string with encoded values + * @param params - Dictionary to encode + * @returns string with encoded values */ function paramsToString(params: Record): string { const values = []; + for (const key in params) { const value = params[key]; + null !== value && void 0 !== value && '' !== value && values.push( - encodeURIComponent(key) + '=' + encodeURIComponent(value) + `${encodeURIComponent(key)}=${encodeURIComponent(value)}` ); } + return values.join('&'); } /** * Make an ajax request * - * @param {any} request RequestInit-like structure but with url/type/timeout parameters as well - * @returns {Promise} response promise, may be automatically unpacked based on request datatype + * @param request - RequestInit-like structure but with url/type/timeout parameters as well + * @returns response promise, may be automatically unpacked based on request datatype */ -export function ajax(request: any): Promise { - if (!request) throw new Error('Request cannot be null'); +export async function ajax(request: any): Promise { + if (!request) { + throw new Error('Request cannot be null'); + } + request.headers = request.headers || {}; - console.log('requesting url: ' + request.url); + console.log(`requesting url: ${request.url}`); - return getFetchPromise(request).then( - (response: Response) => { - console.log( - 'response status: ' + response.status + ', url: ' + request.url - ); - if (response.status >= 400) { - return Promise.reject(response); - } else if ( - request.dataType === 'json' || - request.headers?.accept === 'application/json' - ) { - return response.json(); - } else if ( - request.dataType === 'text' || - (response.headers.get('Content-Type') || '') - .toLowerCase() - .indexOf('text/') === 0 - ) { - return response.text(); - } else { - return response; - } - }, - function (err) { - console.log('request failed to url: ' + request.url); - throw err; + try { + const response = await getFetchPromise(request); + + console.log(`response status: ${response.status}, url: ${request.url}`); + + if (response.status >= 400) { + return Promise.reject(response); + } else if ( + request.dataType === 'json' || + request.headers?.accept === 'application/json' + ) { + return response.json(); + } else if ( + request.dataType === 'text' || + (response.headers.get('Content-Type') || '') + .toLowerCase() + .indexOf('text/') === 0 + ) { + return response.text(); + } else { + return response; } - ); + } catch (err) { + console.log(`request failed to url: ${request.url}`); + throw err; + } } diff --git a/src/components/jellyfinActions.ts b/src/components/jellyfinActions.ts index 5f9b9561..0d408bba 100644 --- a/src/components/jellyfinActions.ts +++ b/src/components/jellyfinActions.ts @@ -32,8 +32,8 @@ let lastTranscoderPing = 0; * * This is used to keep the transcode available during pauses * - * @param $scope global context - * @param reportingParams parameters to report to the server + * @param $scope - global context + * @param reportingParams - parameters to report to the server */ function restartPingInterval( $scope: GlobalScope, @@ -42,7 +42,7 @@ function restartPingInterval( stopPingInterval(); if (reportingParams.PlayMethod == 'Transcode') { - pingInterval = setInterval(function () { + pingInterval = setInterval(() => { pingTranscoder(reportingParams); }, 1000); } @@ -63,8 +63,8 @@ export function stopPingInterval(): void { /** * Report to the server that playback has started. * - * @param $scope global scope - * @param reportingParams parameters to send to the server + * @param $scope - global scope + * @param reportingParams - parameters to send to the server * @returns promise to wait for the request */ export function reportPlaybackStart( @@ -79,27 +79,27 @@ export function reportPlaybackStart( broadcastToMessageBus({ //TODO: convert these to use a defined type in the type field - type: 'playbackstart', - data: getSenderReportingData($scope, reportingParams) + data: getSenderReportingData($scope, reportingParams), + type: 'playbackstart' }); restartPingInterval($scope, reportingParams); return JellyfinApi.authAjax('Sessions/Playing', { - type: 'POST', + contentType: 'application/json', data: JSON.stringify(reportingParams), - contentType: 'application/json' + type: 'POST' }); } /** * Report to the server the progress of the playback. * - * @param $scope global scope - * @param reportingParams parameters for jellyfin - * @param reportToServer if jellyfin should be informed - * @param broadcastEventName name of event to send to the cast sender - * @returns {Promise} Promise for the http request + * @param $scope - global scope + * @param reportingParams - parameters for jellyfin + * @param reportToServer - if jellyfin should be informed + * @param broadcastEventName - name of event to send to the cast sender + * @returns Promise for the http request */ export function reportPlaybackProgress( $scope: GlobalScope, @@ -108,8 +108,8 @@ export function reportPlaybackProgress( broadcastEventName = 'playbackprogress' ): Promise { broadcastToMessageBus({ - type: broadcastEventName, - data: getSenderReportingData($scope, reportingParams) + data: getSenderReportingData($scope, reportingParams), + type: broadcastEventName }); if (reportToServer === false) { @@ -120,17 +120,17 @@ export function reportPlaybackProgress( lastTranscoderPing = new Date().getTime(); return JellyfinApi.authAjax('Sessions/Playing/Progress', { - type: 'POST', + contentType: 'application/json', data: JSON.stringify(reportingParams), - contentType: 'application/json' + type: 'POST' }); } /** * Report to the server that playback has stopped. * - * @param $scope global scope - * @param reportingParams parameters to send to the server + * @param $scope - global scope + * @param reportingParams - parameters to send to the server * @returns promise for waiting for the request */ export function reportPlaybackStopped( @@ -140,14 +140,14 @@ export function reportPlaybackStopped( stopPingInterval(); broadcastToMessageBus({ - type: 'playbackstop', - data: getSenderReportingData($scope, reportingParams) + data: getSenderReportingData($scope, reportingParams), + type: 'playbackstop' }); return JellyfinApi.authAjax('Sessions/Playing/Stopped', { - type: 'POST', + contentType: 'application/json', data: JSON.stringify(reportingParams), - contentType: 'application/json' + type: 'POST' }); } @@ -157,7 +157,7 @@ export function reportPlaybackStopped( * The web client calls that during pause, but this endpoint gets the job done * as well. * - * @param reportingParams progress information to carry + * @param reportingParams - progress information to carry * @returns promise for waiting for the request */ export function pingTranscoder( @@ -168,7 +168,8 @@ export function pingTranscoder( // 10s is the timeout value, so use half that to report often enough if (now - lastTranscoderPing < 5000) { console.debug('Skipping ping due to recent progress check-in'); - return new Promise(function (resolve) { + + return new Promise((resolve) => { resolve(undefined); }); } @@ -177,14 +178,14 @@ export function pingTranscoder( // 10.7 oddly wants it as a query string parameter. This is a server bug for now. return JellyfinApi.authAjax( - 'Sessions/Playing/Ping?playSessionId=' + reportingParams.PlaySessionId, + `Sessions/Playing/Ping?playSessionId=${reportingParams.PlaySessionId}`, { - type: 'POST', + contentType: 'application/json', data: JSON.stringify({ // jellyfin <= 10.6 wants it in the post data. PlaySessionId: reportingParams.PlaySessionId }), - contentType: 'application/json' + type: 'POST' } ); } @@ -192,9 +193,9 @@ export function pingTranscoder( /** * Update the context about the item we are playing. * - * @param $scope global context - * @param customData data to set on $scope - * @param serverItem item that is playing + * @param $scope - global context + * @param customData - data to set on $scope + * @param serverItem - item that is playing */ export function load( $scope: GlobalScope, @@ -219,7 +220,7 @@ export function load( * * TODO: rename these * - * @param $scope global scope + * @param $scope - global scope */ export function play($scope: GlobalScope): void { if ( @@ -228,7 +229,7 @@ export function play($scope: GlobalScope): void { DocumentManager.getAppStatus() == 'playing' || DocumentManager.getAppStatus() == 'audio' ) { - setTimeout(function () { + setTimeout(() => { window.mediaManager.play(); if ($scope.mediaType == 'Audio') { @@ -244,11 +245,21 @@ export function play($scope: GlobalScope): void { * Don't actually stop, just show the idle view after 20ms */ export function stop(): void { - setTimeout(function () { + setTimeout(() => { DocumentManager.setAppStatus('waiting'); }, 20); } +/** + * @param item + * @param maxBitrate + * @param deviceProfile + * @param startPosition + * @param mediaSourceId + * @param audioStreamIndex + * @param subtitleStreamIndex + * @param liveStreamId + */ export function getPlaybackInfo( item: BaseItemDto, maxBitrate: number, @@ -265,33 +276,46 @@ export function getPlaybackInfo( // TODO: PlayRequestQuery might not be the proper type for this const query: PlayRequestQuery = { - UserId: JellyfinApi.userId ?? undefined, + MaxStreamingBitrate: maxBitrate, StartTimeTicks: startPosition || 0, - MaxStreamingBitrate: maxBitrate + UserId: JellyfinApi.userId ?? undefined }; if (audioStreamIndex != null) { query.AudioStreamIndex = audioStreamIndex; } + if (subtitleStreamIndex != null) { query.SubtitleStreamIndex = subtitleStreamIndex; } + if (mediaSourceId) { query.MediaSourceId = mediaSourceId; } + if (liveStreamId) { query.LiveStreamId = liveStreamId; } - return JellyfinApi.authAjax('Items/' + item.Id + '/PlaybackInfo', { - query: query, - type: 'POST', - dataType: 'json', + return JellyfinApi.authAjax(`Items/${item.Id}/PlaybackInfo`, { + contentType: 'application/json', data: JSON.stringify(postData), - contentType: 'application/json' + dataType: 'json', + query: query, + type: 'POST' }); } +/** + * @param item + * @param playSessionId + * @param maxBitrate + * @param deviceProfile + * @param startPosition + * @param mediaSource + * @param audioStreamIndex + * @param subtitleStreamIndex + */ export function getLiveStream( item: BaseItemDto, playSessionId: string, @@ -308,26 +332,27 @@ export function getLiveStream( }; const query: PlayRequestQuery = { - UserId: JellyfinApi.userId ?? undefined, - StartTimeTicks: startPosition || 0, ItemId: item.Id, MaxStreamingBitrate: maxBitrate, - PlaySessionId: playSessionId + PlaySessionId: playSessionId, + StartTimeTicks: startPosition || 0, + UserId: JellyfinApi.userId ?? undefined }; if (audioStreamIndex != null) { query.AudioStreamIndex = audioStreamIndex; } + if (subtitleStreamIndex != null) { query.SubtitleStreamIndex = subtitleStreamIndex; } return JellyfinApi.authAjax('LiveStreams/Open', { - query: query, - type: 'POST', - dataType: 'json', + contentType: 'application/json', data: JSON.stringify(postData), - contentType: 'application/json' + dataType: 'json', + query: query, + type: 'POST' }); } @@ -336,56 +361,51 @@ export function getLiveStream( * * The API has a 10MB limit. * - * @param byteSize number of bytes to request + * @param byteSize - number of bytes to request * @returns the bitrate in bits/s */ -export function getDownloadSpeed(byteSize: number): Promise { - const path = 'Playback/BitrateTest?size=' + byteSize; +export async function getDownloadSpeed(byteSize: number): Promise { + const path = `Playback/BitrateTest?size=${byteSize}`; const now = new Date().getTime(); - return JellyfinApi.authAjax(path, { - type: 'GET', - timeout: 5000 - }) - .then(function (response) { - // Need to wait for the whole response before calculating speed - return response.blob(); - }) - .then(function () { - const responseTimeSeconds = (new Date().getTime() - now) / 1000; - const bytesPerSecond = byteSize / responseTimeSeconds; - const bitrate = Math.round(bytesPerSecond * 8); - - return bitrate; - }); + await JellyfinApi.authAjax(path, { + timeout: 5000, + type: 'GET' + }); + + const responseTimeSeconds = (new Date().getTime() - now) / 1000; + const bytesPerSecond = byteSize / responseTimeSeconds; + const bitrate = Math.round(bytesPerSecond * 8); + + return bitrate; } /** * Function to detect the bitrate. * It first tries 1MB and if bitrate is above 1Mbit/s it tries again with 2.4MB. * - * @returns {Promise} bitrate in bits/s + * @returns bitrate in bits/s */ -export function detectBitrate(): Promise { +export async function detectBitrate(): Promise { // First try a small amount so that we don't hang up their mobile connection - return getDownloadSpeed(1000000).then(function (bitrate) { - if (bitrate < 1000000) { - return Math.round(bitrate * 0.8); - } else { - // If that produced a fairly high speed, try again with a larger size to get a more accurate result - return getDownloadSpeed(2400000).then(function (bitrate) { - return Math.round(bitrate * 0.8); - }); - } - }); + + let bitrate = await getDownloadSpeed(1000000); + + if (bitrate < 1000000) { + return Math.round(bitrate * 0.8); + } + + bitrate = await getDownloadSpeed(2400000); + + return Math.round(bitrate * 0.8); } /** * Tell Jellyfin to kill off our active transcoding session * - * @param {GlobalScope} $scope Global scope variable - * @returns {Promise} Promise for the http request to go through + * @param $scope - Global scope variable + * @returns Promise for the http request to go through */ export function stopActiveEncodings($scope: GlobalScope): Promise { const options = { @@ -398,7 +418,7 @@ export function stopActiveEncodings($scope: GlobalScope): Promise { } return JellyfinApi.authAjax('Videos/ActiveEncodings', { - type: 'DELETE', - query: options + query: options, + type: 'DELETE' }); } diff --git a/src/components/jellyfinApi.ts b/src/components/jellyfinApi.ts index c463d230..8221ea07 100644 --- a/src/components/jellyfinApi.ts +++ b/src/components/jellyfinApi.ts @@ -42,8 +42,9 @@ export abstract class JellyfinApi { Authorization: auth }; - if (this.accessToken != null) + if (this.accessToken != null) { headers['X-MediaBrowser-Token'] = this.accessToken; + } return headers; } @@ -53,31 +54,35 @@ export abstract class JellyfinApi { public static createUrl(path: string): string { if (this.serverAddress === null) { console.error('JellyfinApi.createUrl: no server address present'); + return ''; } + // Remove leading slashes - while (path.charAt(0) === '/') path = path.substring(1); + while (path.charAt(0) === '/') { + path = path.substring(1); + } - return this.serverAddress + '/' + path; + return `${this.serverAddress}/${path}`; } // create a path in /Users/userId/ public static createUserUrl(path: string | null = null): string { if (path) { - return this.createUrl('Users/' + this.userId + '/' + path); + return this.createUrl(`Users/${this.userId}/${path}`); } else { - return this.createUrl('Users/' + this.userId); + return this.createUrl(`Users/${this.userId}`); } } /** * Create url to image * - * @param {string} itemId Item id - * @param {string} imgType Image type: Primary, Logo, Backdrop - * @param {string} imgTag Image tag - * @param {number} imgIdx Image index, default 0 - * @returns {string} URL + * @param itemId - Item id + * @param imgType - Image type: Primary, Logo, Backdrop + * @param imgTag - Image tag + * @param imgIdx - Image index, default 0 + * @returns URL */ public static createImageUrl( itemId: string, @@ -100,11 +105,13 @@ export abstract class JellyfinApi { console.error( 'JellyfinApi.authAjax: No userid/accesstoken/serverAddress present. Skipping request' ); + return Promise.reject('no server info present'); } + const params = { - url: this.createUrl(path), - headers: this.getSecurityHeaders() + headers: this.getSecurityHeaders(), + url: this.createUrl(path) }; return ajax({ ...params, ...args }); @@ -120,11 +127,13 @@ export abstract class JellyfinApi { console.error( 'JellyfinApi.authAjaxUser: No userid/accesstoken/serverAddress present. Skipping request' ); + return Promise.reject('no server info present'); } + const params = { - url: this.createUserUrl(path), - headers: this.getSecurityHeaders() + headers: this.getSecurityHeaders(), + url: this.createUserUrl(path) }; return ajax({ ...params, ...args }); diff --git a/src/components/maincontroller.ts b/src/components/maincontroller.ts index 9dc037e7..b51980c7 100644 --- a/src/components/maincontroller.ts +++ b/src/components/maincontroller.ts @@ -27,8 +27,6 @@ import { playbackManager } from './playbackManager'; import { CommandHandler } from './commandHandler'; import { getMaxBitrateSupport } from './codecSupportHelper'; import { DocumentManager } from './documentManager'; - -import { BaseItemDtoQueryResult } from '~/api/generated/models/base-item-dto-query-result'; import { BaseItemDto } from '~/api/generated/models/base-item-dto'; import { MediaSourceInfo } from '~/api/generated/models/media-source-info'; import { GlobalScope, PlayRequest } from '~/types/global'; @@ -48,6 +46,9 @@ let broadcastToServer = new Date(); let hasReportedCapabilities = false; +/** + * + */ export function onMediaElementTimeUpdate(): void { if ($scope.isChangingStream) { return; @@ -67,6 +68,9 @@ export function onMediaElementTimeUpdate(): void { } } +/** + * + */ export function onMediaElementPause(): void { if ($scope.isChangingStream) { return; @@ -75,19 +79,32 @@ export function onMediaElementPause(): void { reportEvent('playstatechange', true); } +/** + * + */ export function onMediaElementPlaying(): void { if ($scope.isChangingStream) { return; } + reportEvent('playstatechange', true); } +/** + * @param event + */ function onMediaElementVolumeChange(event: framework.system.Event): void { window.volume = (event).data; - console.log('Received volume update: ' + window.volume.level); - if (JellyfinApi.serverAddress !== null) reportEvent('volumechange', true); + console.log(`Received volume update: ${window.volume.level}`); + + if (JellyfinApi.serverAddress !== null) { + reportEvent('volumechange', true); + } } +/** + * + */ export function enableTimeUpdateListener(): void { window.mediaManager.addEventListener( cast.framework.events.EventType.TIME_UPDATE, @@ -107,6 +124,9 @@ export function enableTimeUpdateListener(): void { ); } +/** + * + */ export function disableTimeUpdateListener(): void { window.mediaManager.removeEventListener( cast.framework.events.EventType.TIME_UPDATE, @@ -128,7 +148,7 @@ export function disableTimeUpdateListener(): void { enableTimeUpdateListener(); -window.addEventListener('beforeunload', function () { +window.addEventListener('beforeunload', () => { // Try to cleanup after ourselves before the page closes disableTimeUpdateListener(); reportPlaybackStopped($scope, getReportingParams($scope)); @@ -143,6 +163,9 @@ mgr.addEventListener(cast.framework.events.EventType.PAUSE, (): void => { reportPlaybackProgress($scope, getReportingParams($scope)); }); +/** + * + */ function defaultOnStop(): void { playbackMgr.stop(); } @@ -153,7 +176,7 @@ mgr.addEventListener( ); mgr.addEventListener(cast.framework.events.EventType.ABORT, defaultOnStop); -mgr.addEventListener(cast.framework.events.EventType.ENDED, function () { +mgr.addEventListener(cast.framework.events.EventType.ENDED, () => { // Ignore if ($scope.isChangingStream) { return; @@ -180,29 +203,36 @@ window.mediaManager.addEventListener( } ); -export function reportDeviceCapabilities(): Promise { - return getMaxBitrate().then((maxBitrate) => { - const deviceProfile = getDeviceProfile({ - enableHls: true, - bitrateSetting: maxBitrate - }); +/** + * + */ +export async function reportDeviceCapabilities(): Promise { + const maxBitrate = await getMaxBitrate(); - const capabilities = { - PlayableMediaTypes: ['Audio', 'Video'], - SupportsPersistentIdentifier: false, - SupportsMediaControl: true, - DeviceProfile: deviceProfile - }; - hasReportedCapabilities = true; - - return JellyfinApi.authAjax('Sessions/Capabilities/Full', { - type: 'POST', - data: JSON.stringify(capabilities), - contentType: 'application/json' - }); + const deviceProfile = getDeviceProfile({ + bitrateSetting: maxBitrate, + enableHls: true + }); + + const capabilities = { + DeviceProfile: deviceProfile, + PlayableMediaTypes: ['Audio', 'Video'], + SupportsMediaControl: true, + SupportsPersistentIdentifier: false + }; + + hasReportedCapabilities = true; + + return JellyfinApi.authAjax('Sessions/Capabilities/Full', { + contentType: 'application/json', + data: JSON.stringify(capabilities), + type: 'POST' }); } +/** + * @param data + */ export function processMessage(data: any): void { if ( !data.command || @@ -213,10 +243,11 @@ export function processMessage(data: any): void { console.log('Invalid message sent from sender. Sending error response'); broadcastToMessageBus({ - type: 'error', message: - 'Missing one or more required params - command,options,userId,accessToken,serverAddress' + 'Missing one or more required params - command,options,userId,accessToken,serverAddress', + type: 'error' }); + return; } @@ -238,7 +269,9 @@ export function processMessage(data: any): void { } data.options = data.options || {}; + const cleanReceiverName = cleanName(data.receiverName || ''); + window.deviceInfo.deviceName = cleanReceiverName || window.deviceInfo.deviceName; // deviceId just needs to be unique-ish @@ -255,6 +288,7 @@ export function processMessage(data: any): void { if (window.reportEventType) { const report = (): Promise => reportPlaybackProgress($scope, getReportingParams($scope)); + reportPlaybackProgress( $scope, getReportingParams($scope), @@ -266,6 +300,10 @@ export function processMessage(data: any): void { } } +/** + * @param name + * @param reportToServer + */ export function reportEvent( name: string, reportToServer: boolean @@ -278,16 +316,20 @@ export function reportEvent( ); } +/** + * @param $scope + * @param index + */ export function setSubtitleStreamIndex( $scope: GlobalScope, index: number ): void { - console.log('setSubtitleStreamIndex. index: ' + index); + console.log(`setSubtitleStreamIndex. index: ${index}`); let positionTicks; const currentSubtitleStream = $scope.mediaSource.MediaStreams.filter( - function (m: any) { + (m: any) => { return ( m.Index == $scope.subtitleStreamIndex && m.Type == 'Subtitle' ); @@ -309,6 +351,7 @@ export function setSubtitleStreamIndex( $scope.subtitleStreamIndex = -1; setTextTrack(null); } + return; } @@ -320,11 +363,12 @@ export function setSubtitleStreamIndex( console.log( 'setSubtitleStreamIndex error condition - subtitle stream not found.' ); + return; } console.log( - 'setSubtitleStreamIndex DeliveryMethod:' + subtitleStream.DeliveryMethod + `setSubtitleStreamIndex DeliveryMethod:${subtitleStream.DeliveryMethod}` ); if ( @@ -335,9 +379,10 @@ export function setSubtitleStreamIndex( ? subtitleStream.DeliveryUrl : JellyfinApi.createUrl(subtitleStream.DeliveryUrl); - console.log('Subtitle url: ' + textStreamUrl); + console.log(`Subtitle url: ${textStreamUrl}`); setTextTrack(index); $scope.subtitleStreamIndex = subtitleStream.Index; + return; } else { console.log('setSubtitleStreamIndex video url change required'); @@ -348,21 +393,33 @@ export function setSubtitleStreamIndex( } } +/** + * @param $scope + * @param index + */ export function setAudioStreamIndex( $scope: GlobalScope, index: number ): Promise { const positionTicks = getCurrentPositionTicks($scope); + return changeStream(positionTicks, { AudioStreamIndex: index }); } +/** + * @param ticks + */ export function seek(ticks: number): Promise { return changeStream(ticks); } -export function changeStream( +/** + * @param ticks + * @param params + */ +export async function changeStream( ticks: number, params: any = undefined ): Promise { @@ -372,6 +429,7 @@ export function changeStream( ) { window.mediaManager.seek(ticks / 10000000); reportPlaybackProgress($scope, getReportingParams($scope)); + return Promise.resolve(); } @@ -381,70 +439,74 @@ export function changeStream( const liveStreamId = $scope.liveStreamId; const item = $scope.item; + const maxBitrate = await getMaxBitrate(); - return getMaxBitrate().then(async (maxBitrate) => { - const deviceProfile = getDeviceProfile({ - enableHls: true, - bitrateSetting: maxBitrate - }); - const audioStreamIndex = - params.AudioStreamIndex == null - ? $scope.audioStreamIndex - : params.AudioStreamIndex; - const subtitleStreamIndex = - params.SubtitleStreamIndex == null - ? $scope.subtitleStreamIndex - : params.SubtitleStreamIndex; - - const playbackInformation = await getPlaybackInfo( - item, - maxBitrate, - deviceProfile, - ticks, - $scope.mediaSourceId, - audioStreamIndex, - subtitleStreamIndex, - liveStreamId - ); - if (!validatePlaybackInfoResult(playbackInformation)) { - return; - } + const deviceProfile = getDeviceProfile({ + bitrateSetting: maxBitrate, + enableHls: true + }); + const audioStreamIndex = + params.AudioStreamIndex == null + ? $scope.audioStreamIndex + : params.AudioStreamIndex; + const subtitleStreamIndex = + params.SubtitleStreamIndex == null + ? $scope.subtitleStreamIndex + : params.SubtitleStreamIndex; + + const playbackInformation = await getPlaybackInfo( + item, + maxBitrate, + deviceProfile, + ticks, + $scope.mediaSourceId, + audioStreamIndex, + subtitleStreamIndex, + liveStreamId + ); - const mediaSource = playbackInformation.MediaSources[0]; - const streamInfo = createStreamInfo(item, mediaSource, ticks); + if (!validatePlaybackInfoResult(playbackInformation)) { + return; + } - if (!streamInfo.url) { - showPlaybackInfoErrorMessage('NoCompatibleStream'); - return; - } + const mediaSource = playbackInformation.MediaSources[0]; + const streamInfo = createStreamInfo(item, mediaSource, ticks); - const mediaInformation = createMediaInformation( - playSessionId, - item, - streamInfo - ); - const loadRequest = new cast.framework.messages.LoadRequestData(); - loadRequest.media = mediaInformation; - loadRequest.autoplay = true; - - // TODO something to do with HLS? - const requiresStoppingTranscoding = false; - if (requiresStoppingTranscoding) { - window.mediaManager.pause(); - await stopActiveEncodings(playSessionId); - } - window.mediaManager.load(loadRequest); - window.mediaManager.play(); - $scope.subtitleStreamIndex = subtitleStreamIndex; - $scope.audioStreamIndex = audioStreamIndex; - }); + if (!streamInfo.url) { + showPlaybackInfoErrorMessage('NoCompatibleStream'); + + return; + } + + const mediaInformation = createMediaInformation( + playSessionId, + item, + streamInfo + ); + const loadRequest = new cast.framework.messages.LoadRequestData(); + + loadRequest.media = mediaInformation; + loadRequest.autoplay = true; + + // TODO something to do with HLS? + const requiresStoppingTranscoding = false; + + if (requiresStoppingTranscoding) { + window.mediaManager.pause(); + await stopActiveEncodings(playSessionId); + } + + window.mediaManager.load(loadRequest); + window.mediaManager.play(); + $scope.subtitleStreamIndex = subtitleStreamIndex; + $scope.audioStreamIndex = audioStreamIndex; } // Create a message handler for the custome namespace channel // TODO save namespace somewhere global? window.castReceiverContext.addCustomMessageListener( 'urn:x-cast:com.connectsdk', - function (evt: any) { + (evt: any) => { let data: any = evt.data; // Apparently chromium likes to pass it as json, not as object. @@ -459,151 +521,181 @@ window.castReceiverContext.addCustomMessageListener( // TODO set it somewhere better perhaps window.senderId = evt.senderId; - console.log('Received message: ' + JSON.stringify(data)); + console.log(`Received message: ${JSON.stringify(data)}`); processMessage(data); } ); -export function translateItems( +/** + * @param data + * @param options + * @param method + */ +export async function translateItems( data: any, options: PlayRequest, method: string ): Promise { const playNow = method != 'PlayNext' && method != 'PlayLast'; - return translateRequestedItems(data.userId, options.items, playNow).then( - function (result: BaseItemDtoQueryResult) { - if (result.Items) options.items = result.Items; - - if (method == 'PlayNext' || method == 'PlayLast') { - for ( - let i = 0, length = options.items.length; - i < length; - i++ - ) { - window.playlist.push(options.items[i]); - } - } else { - playbackMgr.playFromOptions(data.options); - } - } + + const result = await translateRequestedItems( + data.userId, + options.items, + playNow ); + + if (result.Items) { + options.items = result.Items; + } + + if (method == 'PlayNext' || method == 'PlayLast') { + for (let i = 0, length = options.items.length; i < length; i++) { + window.playlist.push(options.items[i]); + } + } else { + playbackMgr.playFromOptions(data.options); + } } -export function instantMix( +/** + * @param data + * @param options + * @param item + */ +export async function instantMix( data: any, options: any, item: BaseItemDto ): Promise { - return getInstantMixItems(data.userId, item).then(function (result) { - options.items = result.Items; - playbackMgr.playFromOptions(data.options); - }); + const result = await getInstantMixItems(data.userId, item); + + options.items = result.Items; + playbackMgr.playFromOptions(data.options); } -export function shuffle( +/** + * @param data + * @param options + * @param item + */ +export async function shuffle( data: any, options: any, item: BaseItemDto ): Promise { - return getShuffleItems(data.userId, item).then(function (result) { - options.items = result.Items; - playbackMgr.playFromOptions(data.options); - }); + const result = await getShuffleItems(data.userId, item); + + options.items = result.Items; + playbackMgr.playFromOptions(data.options); } -export function onStopPlayerBeforePlaybackDone( +/** + * @param item + * @param options + */ +export async function onStopPlayerBeforePlaybackDone( item: BaseItemDto, options: any ): Promise { - return JellyfinApi.authAjaxUser('Items/' + item.Id, { + const data = await JellyfinApi.authAjaxUser(`Items/${item.Id}`, { dataType: 'json', type: 'GET' - }).then(function (data) { - // Attach the custom properties we created like userId, serverAddress, itemId, etc - extend(data, item); + }); - playbackMgr.playItemInternal(data, options); - }, broadcastConnectionErrorMessage); + // Attach the custom properties we created like userId, serverAddress, itemId, etc + extend(data, item); + playbackMgr.playItemInternal(data, options); + broadcastConnectionErrorMessage(); } let lastBitrateDetect = 0; let detectedBitrate = 0; -export function getMaxBitrate(): Promise { +/** + * + */ +export async function getMaxBitrate(): Promise { console.log('getMaxBitrate'); - return new Promise(function (resolve) { - // The client can set this number - if (window.MaxBitrate) { - console.log('bitrate is set to ' + window.MaxBitrate); - - resolve(window.MaxBitrate); - return; - } + if (window.MaxBitrate) { + console.log(`bitrate is set to ${window.MaxBitrate}`); - if ( - detectedBitrate && - new Date().getTime() - lastBitrateDetect < 600000 - ) { - console.log( - 'returning previous detected bitrate of ' + detectedBitrate - ); - resolve(detectedBitrate); - return; - } + return window.MaxBitrate; + } - console.log('detecting bitrate'); - - detectBitrate().then( - (bitrate) => { - console.log('Max bitrate auto detected to ' + bitrate); - lastBitrateDetect = new Date().getTime(); - detectedBitrate = bitrate; - - resolve(detectedBitrate); - }, - () => { - console.log( - 'Error detecting bitrate, will return device maximum.' - ); - resolve(getMaxBitrateSupport()); - } + if (detectedBitrate && new Date().getTime() - lastBitrateDetect < 600000) { + console.log( + `returning previous detected bitrate of ${detectedBitrate}` ); - }); + + return detectedBitrate; + } + + console.log('detecting bitrate'); + + const bitrate = await detectBitrate(); + + try { + console.log(`Max bitrate auto detected to ${bitrate}`); + lastBitrateDetect = new Date().getTime(); + detectedBitrate = bitrate; + + return detectedBitrate; + } catch (e) { + // The client can set this number + console.log('Error detecting bitrate, will return device maximum.'); + + return getMaxBitrateSupport(); + } } +/** + * @param result + */ export function validatePlaybackInfoResult(result: any): boolean { if (result.ErrorCode) { showPlaybackInfoErrorMessage(result.ErrorCode); + return false; } + return true; } +/** + * @param error + */ export function showPlaybackInfoErrorMessage(error: string): void { - broadcastToMessageBus({ type: 'playbackerror', message: error }); + broadcastToMessageBus({ message: error, type: 'playbackerror' }); } +/** + * @param versions + */ export function getOptimalMediaSource(versions: Array): any { - let optimalVersion = versions.filter(function (v) { + let optimalVersion = versions.filter((v) => { checkDirectPlay(v); + return v.SupportsDirectPlay; })[0]; if (!optimalVersion) { - optimalVersion = versions.filter(function (v) { + optimalVersion = versions.filter((v) => { return v.SupportsDirectStream; })[0]; } return ( optimalVersion || - versions.filter(function (s) { + versions.filter((s) => { return s.SupportsTranscoding; })[0] ); } // Disable direct play on non-http sources +/** + * @param mediaSource + */ export function checkDirectPlay(mediaSource: MediaSourceInfo): void { if ( mediaSource.SupportsDirectPlay && @@ -613,16 +705,22 @@ export function checkDirectPlay(mediaSource: MediaSourceInfo): void { ) { return; } + mediaSource.SupportsDirectPlay = false; } +/** + * @param index + */ export function setTextTrack(index: number | null): void { try { const textTracksManager = window.mediaManager.getTextTracksManager(); + if (index == null) { // docs: null is okay // typescript definitions: Must be Array textTracksManager.setActiveByIds([]); + return; } @@ -630,12 +728,17 @@ export function setTextTrack(index: number | null): void { const subtitleTrack: framework.messages.Track | undefined = tracks.find( (track: framework.messages.Track) => { return track.trackId === index; - }); + } + ); + if (subtitleTrack && subtitleTrack.trackId !== undefined) { textTracksManager.setActiveByIds([subtitleTrack.trackId]); + const subtitleAppearance = window.subtitleAppearance; + if (subtitleAppearance) { const textTrackStyle = new cast.framework.messages.TextTrackStyle(); + if (subtitleAppearance.dropShadow != null) { // Empty string is DROP_SHADOW textTrackStyle.edgeType = @@ -650,8 +753,7 @@ export function setTextTrack(index: number | null): void { if (subtitleAppearance.textColor) { // Append the transparency, hardcoded to 100% - textTrackStyle.foregroundColor = - subtitleAppearance.textColor + 'FF'; + textTrackStyle.foregroundColor = `${subtitleAppearance.textColor}FF`; } if (subtitleAppearance.textBackground === 'transparent') { @@ -678,35 +780,42 @@ export function setTextTrack(index: number | null): void { textTrackStyle.fontScale = 1.0; break; } + textTracksManager.setTextTrackStyle(textTrackStyle); } } } catch (e) { - console.log('Setting subtitle track failed: ' + e); + console.log(`Setting subtitle track failed: ${e}`); } } // TODO no any types +/** + * @param playSessionId + * @param item + * @param streamInfo + */ export function createMediaInformation( playSessionId: string, item: BaseItemDto, streamInfo: any ): framework.messages.MediaInformation { const mediaInfo = new cast.framework.messages.MediaInformation(); + mediaInfo.contentId = streamInfo.url; mediaInfo.contentType = streamInfo.contentType; mediaInfo.customData = { - startPositionTicks: streamInfo.startPositionTicks || 0, + audioStreamIndex: streamInfo.audioStreamIndex, + canClientSeek: streamInfo.canClientSeek, + canSeek: streamInfo.canSeek, itemId: item.Id, + liveStreamId: streamInfo.mediaSource.LiveStreamId, mediaSourceId: streamInfo.mediaSource.Id, - audioStreamIndex: streamInfo.audioStreamIndex, - subtitleStreamIndex: streamInfo.subtitleStreamIndex, playMethod: streamInfo.isStatic ? 'DirectStream' : 'Transcode', + playSessionId: playSessionId, runtimeTicks: streamInfo.mediaSource.RunTimeTicks, - liveStreamId: streamInfo.mediaSource.LiveStreamId, - canSeek: streamInfo.canSeek, - canClientSeek: streamInfo.canClientSeek, - playSessionId: playSessionId + startPositionTicks: streamInfo.startPositionTicks || 0, + subtitleStreamIndex: streamInfo.subtitleStreamIndex }; mediaInfo.metadata = getMetadata(item); @@ -727,6 +836,7 @@ export function createMediaInformation( // Set the available buttons in the UI controls. const controls = cast.framework.ui.Controls.getInstance(); + controls.clearDefaultSlotAssignments(); /* Disabled for now, dynamically set controls for each media type in the future. @@ -746,6 +856,7 @@ controls.assignButton( ); const options = new cast.framework.CastReceiverOptions(); + // Global variable set by Webpack if (!PRODUCTION) { window.castReceiverContext.setLoggerLevel(cast.framework.LoggerLevel.DEBUG); @@ -760,7 +871,7 @@ if (!PRODUCTION) { window.mediaManager.addEventListener( cast.framework.events.category.CORE, (event: framework.events.Event) => { - console.log('Core event: ' + event.type); + console.log(`Core event: ${event.type}`); console.log(event); } ); diff --git a/src/components/playbackManager.ts b/src/components/playbackManager.ts index f628c1a4..f59b9786 100644 --- a/src/components/playbackManager.ts +++ b/src/components/playbackManager.ts @@ -68,7 +68,9 @@ export class playbackManager { } const intros = await getIntros(firstItem); + options.items = intros.Items?.concat(options.items); + return this.playFromOptionsInternal(options); } @@ -92,6 +94,7 @@ export class playbackManager { const item = nextItemInfo.item; this.playItem(item, options, stopPlayer); + return true; } @@ -105,8 +108,10 @@ export class playbackManager { const item = this.activePlaylist[this.activePlaylistIndex]; this.playItem(item, options, true); + return true; } + return false; } @@ -128,8 +133,8 @@ export class playbackManager { const maxBitrate = await getMaxBitrate(); const deviceProfile = getDeviceProfile({ - enableHls: true, - bitrateSetting: maxBitrate + bitrateSetting: maxBitrate, + enableHls: true }); const playbackInfo = await getPlaybackInfo( item, @@ -148,11 +153,13 @@ export class playbackManager { const mediaSource = await getOptimalMediaSource( playbackInfo.MediaSources ); + if (!mediaSource) { return showPlaybackInfoErrorMessage('NoCompatibleStream'); } let itemToPlay = mediaSource; + if (mediaSource.RequiresOpening) { const openLiveStreamResult = await getLiveStream( item, @@ -164,6 +171,7 @@ export class playbackManager { null, null ); + if (openLiveStreamResult.MediaSource) { checkDirectPlay(openLiveStreamResult.MediaSource); itemToPlay = openLiveStreamResult.MediaSource; @@ -201,6 +209,7 @@ export class playbackManager { streamInfo ); const loadRequestData = new cast.framework.messages.LoadRequestData(); + loadRequestData.media = mediaInfo; loadRequestData.autoplay = true; @@ -209,7 +218,7 @@ export class playbackManager { $scope.PlaybackMediaSource = mediaSource; - console.log('setting src to ' + url); + console.log(`setting src to ${url}`); $scope.mediaSource = mediaSource; DocumentManager.setPlayerBackdrop(item); diff --git a/src/helpers.ts b/src/helpers.ts index 43d468a5..962433bd 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -12,12 +12,13 @@ import { GlobalScope, BusMessage, ItemIndex, ItemQuery } from './types/global'; /** * Get current playback position in ticks, adjusted for server seeking * - * @param $scope global context variable + * @param $scope - global context variable * @returns position in ticks */ export function getCurrentPositionTicks($scope: GlobalScope): number { let positionTicks = window.mediaManager.getCurrentTimeSec() * 10000000; const mediaInformation = window.mediaManager.getMediaInformation(); + if (mediaInformation && !mediaInformation.customData.canClientSeek) { positionTicks += $scope.startPositionTicks || 0; } @@ -28,7 +29,7 @@ export function getCurrentPositionTicks($scope: GlobalScope): number { /** * Get parameters used for playback reporting * - * @param $scope global context variable + * @param $scope - global context variable * @returns progress information for use with the reporting APIs */ export function getReportingParams($scope: GlobalScope): PlaybackProgressInfo { @@ -38,21 +39,21 @@ export function getReportingParams($scope: GlobalScope): PlaybackProgressInfo { * those fields are always rounded. */ return { - PositionTicks: Math.round(getCurrentPositionTicks($scope)), + AudioStreamIndex: $scope.audioStreamIndex, + CanSeek: $scope.canSeek, + IsMuted: window.volume.muted, IsPaused: window.mediaManager.getPlayerState() === cast.framework.messages.PlayerState.PAUSED, - IsMuted: window.volume.muted, - AudioStreamIndex: $scope.audioStreamIndex, - SubtitleStreamIndex: $scope.subtitleStreamIndex, - VolumeLevel: Math.round(window.volume.level * 100), ItemId: $scope.itemId, + LiveStreamId: $scope.liveStreamId, MediaSourceId: $scope.mediaSourceId, - CanSeek: $scope.canSeek, PlayMethod: $scope.playMethod, - LiveStreamId: $scope.liveStreamId, PlaySessionId: $scope.playSessionId, - RepeatMode: window.repeatMode + PositionTicks: Math.round(getCurrentPositionTicks($scope)), + RepeatMode: window.repeatMode, + SubtitleStreamIndex: $scope.subtitleStreamIndex, + VolumeLevel: Math.round(window.volume.level * 100) }; } @@ -79,9 +80,11 @@ export function getNextPlaybackItemInfo(): ItemIndex | null { break; case 'RepeatAll': newIndex = window.currentPlaylistIndex + 1; + if (newIndex >= window.playlist.length) { newIndex = 0; } + break; default: newIndex = window.currentPlaylistIndex + 1; @@ -93,10 +96,11 @@ export function getNextPlaybackItemInfo(): ItemIndex | null { const item = playlist[newIndex]; return { - item: item, - index: newIndex + index: newIndex, + item: item }; } + return null; } @@ -105,8 +109,8 @@ export function getNextPlaybackItemInfo(): ItemIndex | null { * about the item that is currently playing. This is sent over the cast protocol over to * the connected client (or clients?). * - * @param $scope global context - * @param reportingData object full of random information + * @param $scope - global context + * @param reportingData - object full of random information * @returns lots of data for the connected client */ export function getSenderReportingData( @@ -133,7 +137,7 @@ export function getSenderReportingData( nowPlayingItem.Chapters = item.Chapters || []; // TODO: Fill these - const mediaSource = item.MediaSources.filter(function (m: any) { + const mediaSource = item.MediaSources.filter((m: any) => { return m.Id == reportingData.MediaSourceId; })[0]; @@ -206,7 +210,7 @@ export function getSenderReportingData( /** * Attempt to clean the receiver state. * - * @param $scope global context variable + * @param $scope - global context variable */ export function resetPlaybackScope($scope: GlobalScope): void { DocumentManager.setAppStatus('waiting'); @@ -240,44 +244,52 @@ export function resetPlaybackScope($scope: GlobalScope): void { /** * Create CAF-native metadata for a given item * - * @param item item to look up + * @param item - item to look up * @returns one of the metadata classes in cast.framework.messages.*Metadata */ export function getMetadata(item: BaseItemDto): any { let metadata: any; let posterUrl = ''; - if (item.SeriesPrimaryImageTag) + if (item.SeriesPrimaryImageTag) { posterUrl = JellyfinApi.createUrl( `Items/${item.SeriesId}/Images/Primary?tag=${item.SeriesPrimaryImageTag}` ); - else if (item.AlbumPrimaryImageTag) + } else if (item.AlbumPrimaryImageTag) { posterUrl = JellyfinApi.createUrl( `Items/${item.AlbumId}/Images/Primary?tag=${item.AlbumPrimaryImageTag}` ); - else if (item.ImageTags?.Primary) + } else if (item.ImageTags?.Primary) { posterUrl = JellyfinApi.createUrl( `Items/${item.Id}/Images/Primary?tag=${item.ImageTags.Primary}` ); + } if (item.Type == 'Episode') { metadata = new cast.framework.messages.TvShowMediaMetadata(); metadata.seriesTitle = item.SeriesName; - if (item.PremiereDate) + if (item.PremiereDate) { metadata.originalAirdate = parseISO8601Date( item.PremiereDate ).toISOString(); - if (item.IndexNumber != null) metadata.episode = item.IndexNumber; - if (item.ParentIndexNumber != null) + } + + if (item.IndexNumber != null) { + metadata.episode = item.IndexNumber; + } + + if (item.ParentIndexNumber != null) { metadata.season = item.ParentIndexNumber; + } } else if (item.Type == 'Photo') { metadata = new cast.framework.messages.PhotoMediaMetadata(); - if (item.PremiereDate) + if (item.PremiereDate) { metadata.creationDateTime = parseISO8601Date( item.PremiereDate ).toISOString(); + } // TODO more metadata? } else if (item.Type == 'Audio') { metadata = new cast.framework.messages.MusicTrackMediaMetadata(); @@ -287,37 +299,53 @@ export function getMetadata(item: BaseItemDto): any { metadata.albumArtist = item.AlbumArtist; metadata.albumName = item.Album; - if (item.PremiereDate) + if (item.PremiereDate) { metadata.releaseDate = parseISO8601Date( item.PremiereDate ).toISOString(); - if (item.IndexNumber != null) metadata.trackNumber = item.IndexNumber; - if (item.ParentIndexNumber != null) + } + + if (item.IndexNumber != null) { + metadata.trackNumber = item.IndexNumber; + } + + if (item.ParentIndexNumber != null) { metadata.discNumber = item.ParentIndexNumber; + } + // previously: p.PersonType == 'Type'.. wtf? const composer = (item.People || []).filter( (p: BaseItemPerson) => p.Type == 'Composer' )[0]; - if (composer) metadata.composer = composer.Name; + + if (composer) { + metadata.composer = composer.Name; + } } else if (item.Type == 'Movie') { metadata = new cast.framework.messages.MovieMediaMetadata(); - if (item.PremiereDate) + + if (item.PremiereDate) { metadata.releaseDate = parseISO8601Date( item.PremiereDate ).toISOString(); + } } else { metadata = new cast.framework.messages.GenericMediaMetadata(); - if (item.PremiereDate) + if (item.PremiereDate) { metadata.releaseDate = parseISO8601Date( item.PremiereDate ).toISOString(); - if (item.Studios && item.Studios.length) + } + + if (item.Studios && item.Studios.length) { metadata.studio = item.Studios[0]; + } } metadata.title = item.Name ?? '????'; metadata.images = [new cast.framework.messages.Image(posterUrl)]; + return metadata; } @@ -325,9 +353,9 @@ export function getMetadata(item: BaseItemDto): any { * Create the necessary information about an item * needed for playback * - * @param item Item to play - * @param mediaSource MediaSourceInfo for the item - * @param startPosition Where to seek to (possibly server seeking) + * @param item - Item to play + * @param mediaSource - MediaSourceInfo for the item + * @param startPosition - Where to seek to (possibly server seeking) * @returns object with enough information to start playback */ export function createStreamInfo( @@ -343,7 +371,7 @@ export function createStreamInfo( ? startPosition / 10000000 : 0; const seekParam = startPositionInSeekParam - ? '#t=' + startPositionInSeekParam + ? `#t=${startPositionInSeekParam}` : ''; let isStatic = false; @@ -354,7 +382,7 @@ export function createStreamInfo( const type = item.MediaType?.toLowerCase(); if (type == 'video') { - contentType = 'video/' + mediaSource.Container; + contentType = `video/${mediaSource.Container}`; if (mediaSource.SupportsDirectPlay) { mediaUrl = mediaSource.Path; @@ -377,7 +405,7 @@ export function createStreamInfo( contentType = 'application/x-mpegURL'; streamContainer = 'm3u8'; } else { - contentType = 'video/' + mediaSource.TranscodingContainer; + contentType = `video/${mediaSource.TranscodingContainer}`; streamContainer = mediaSource.TranscodingContainer; if ( @@ -388,7 +416,7 @@ export function createStreamInfo( } } } else { - contentType = 'audio/' + mediaSource.Container; + contentType = `audio/${mediaSource.Container}`; if (mediaSource.SupportsDirectPlay) { mediaUrl = mediaSource.Path; @@ -408,7 +436,7 @@ export function createStreamInfo( isStatic = true; } else { streamContainer = mediaSource.TranscodingContainer; - contentType = 'audio/' + mediaSource.TranscodingContainer; + contentType = `audio/${mediaSource.TranscodingContainer}`; // TODO deal with !TranscodingUrl mediaUrl = JellyfinApi.createUrl( @@ -423,25 +451,26 @@ export function createStreamInfo( const canSeek = (mediaSource.RunTimeTicks || 0) > 0; const info: any = { - url: mediaUrl, - mediaSource: mediaSource, - isStatic: isStatic, + audioStreamIndex: mediaSource.DefaultAudioStreamIndex, + canClientSeek: isStatic || (canSeek && streamContainer == 'm3u8'), + canSeek: canSeek, contentType: contentType, + isStatic: isStatic, + mediaSource: mediaSource, + playerStartPositionTicks: playerStartPositionTicks, + startPositionTicks: startPosition, streamContainer: streamContainer, - canSeek: canSeek, - canClientSeek: isStatic || (canSeek && streamContainer == 'm3u8'), - audioStreamIndex: mediaSource.DefaultAudioStreamIndex, subtitleStreamIndex: mediaSource.DefaultSubtitleStreamIndex, - playerStartPositionTicks: playerStartPositionTicks, - startPositionTicks: startPosition + url: mediaUrl }; const subtitleStreams = - mediaSource.MediaStreams?.filter(function (stream: any) { + mediaSource.MediaStreams?.filter((stream: any) => { return stream.Type === 'Subtitle'; }) ?? []; const subtitleTracks: Array = []; - subtitleStreams.forEach(function (subtitleStream: any) { + + subtitleStreams.forEach((subtitleStream: any) => { if (subtitleStream.DeliveryUrl === undefined) { /* The CAF v3 player only supports vtt currently, * SRT subs can be "transcoded" to vtt by jellyfin. @@ -451,6 +480,7 @@ export function createStreamInfo( **/ return; } + const textStreamUrl = subtitleStream.IsExternalUrl ? subtitleStream.DeliveryUrl : JellyfinApi.createUrl(subtitleStream.DeliveryUrl); @@ -459,6 +489,7 @@ export function createStreamInfo( info.subtitleStreamIndex, cast.framework.messages.TrackType.TEXT ); + track.trackId = subtitleStream.Index; track.trackContentId = textStreamUrl; track.language = subtitleStream.Language; @@ -467,7 +498,7 @@ export function createStreamInfo( track.trackContentType = 'text/vtt'; track.subtype = cast.framework.messages.TextTrackType.SUBTITLES; subtitleTracks.push(track); - console.log('Subtitle url: ' + info.subtitleStreamUrl); + console.log(`Subtitle url: ${info.subtitleStreamUrl}`); }); info.tracks = subtitleTracks; @@ -478,9 +509,9 @@ export function createStreamInfo( /** * Get stream by its index while making a type assertion * - * @param streams array streams to consider - * @param type type of stream - * @param index index of stream + * @param streams - array streams to consider + * @param type - type of stream + * @param index - index of stream * @returns first first matching stream */ export function getStreamByIndex( @@ -488,7 +519,7 @@ export function getStreamByIndex( type: string, index: number ): any { - return streams.filter(function (s) { + return streams.filter((s) => { return s.Type == type && s.Index == index; })[0]; } @@ -506,8 +537,8 @@ const requiredItemFields = 'MediaSources,Chapters'; * * TODO: JellyfinApi.userId should be fine for this. * - * @param userId User ID to look up items with - * @param item Parent item of shuffle search + * @param userId - User ID to look up items with + * @param item - Parent item of shuffle search * @returns items for the queue */ export function getShuffleItems( @@ -515,12 +546,12 @@ export function getShuffleItems( item: BaseItemDto ): Promise { const query: ItemQuery = { - UserId: userId, Fields: requiredItemFields, - Limit: 50, Filters: 'IsNotFolder', + Limit: 50, Recursive: true, - SortBy: 'Random' + SortBy: 'Random', + UserId: userId }; if (item.Type == 'MusicArtist') { @@ -542,18 +573,18 @@ export function getShuffleItems( * * TODO: JellyfinApi.userId should be fine for this. * - * @param userId User ID to look up items with - * @param item Parent item of the search + * @param userId - User ID to look up items with + * @param item - Parent item of the search * @returns items for the queue */ -export function getInstantMixItems( +export async function getInstantMixItems( userId: string, item: BaseItemDto ): Promise { const query: any = { - UserId: userId, Fields: requiredItemFields, - Limit: 50 + Limit: 50, + UserId: userId }; let url: string | null = null; @@ -565,32 +596,32 @@ export function getInstantMixItems( url = 'MusicGenres/InstantMix'; query.Id = item.Id; } else if (item.Type == 'MusicAlbum') { - url = 'Albums/' + item.Id + '/InstantMix'; + url = `Albums/${item.Id}/InstantMix`; } else if (item.Type == 'Audio') { - url = 'Songs/' + item.Id + '/InstantMix'; + url = `Songs/${item.Id}/InstantMix`; } else if (item.Type == 'Playlist') { - url = 'Playlists/' + item.Id + '/InstantMix'; + url = `Playlists/${item.Id}/InstantMix`; } if (url) { return JellyfinApi.authAjax(url, { + dataType: 'json', query: query, - type: 'GET', - dataType: 'json' + type: 'GET' }); } else { - return Promise.reject('InstantMix: Unknown item type: ' + item.Type); + throw new Error(`InstantMix: Unknown item type: ${item.Type}`); } } /** * Get items to be played back * - * @param userId user for the search - * @param query specification on what to search for + * @param userId - user for the search + * @param query - specification on what to search for * @returns items to be played back */ -export function getItemsForPlayback( +export async function getItemsForPlayback( userId: string, query: ItemQuery ): Promise { @@ -600,20 +631,23 @@ export function getItemsForPlayback( query.ExcludeLocationTypes = 'Virtual'; if (query.Ids && query.Ids.split(',').length == 1) { - return JellyfinApi.authAjaxUser('Items/' + query.Ids.split(',')[0], { - type: 'GET', - dataType: 'json' - }).then(function (item) { - return { - Items: [item], - TotalRecordCount: 1 - }; - }); + const item = await JellyfinApi.authAjaxUser( + `Items/${query.Ids.split(',')[0]}`, + { + dataType: 'json', + type: 'GET' + } + ); + + return { + Items: [item], + TotalRecordCount: 1 + }; } else { return JellyfinApi.authAjaxUser('Items', { + dataType: 'json', query: query, - type: 'GET', - dataType: 'json' + type: 'GET' }); } } @@ -621,9 +655,9 @@ export function getItemsForPlayback( /** * Get episodes for a show given by seriesId * - * @param userId userid to use - * @param seriesId series to look up - * @param query query parameters to build on + * @param userId - userid to use + * @param seriesId - series to look up + * @param query - query parameters to build on * @returns episode items */ export function getEpisodesForPlayback( @@ -635,10 +669,10 @@ export function getEpisodesForPlayback( query.Fields = requiredItemFields; query.ExcludeLocationTypes = 'Virtual'; - return JellyfinApi.authAjax('Shows/' + seriesId + '/Episodes', { + return JellyfinApi.authAjax(`Shows/${seriesId}/Episodes`, { + dataType: 'json', query: query, - type: 'GET', - dataType: 'json' + type: 'GET' }); } @@ -646,13 +680,13 @@ export function getEpisodesForPlayback( * Get intros for a given item. This item should be a video * type for this to make sense * - * @param firstItem item to get intros for + * @param firstItem - item to get intros for * @returns intro items */ export function getIntros( firstItem: BaseItemDto ): Promise { - return JellyfinApi.authAjaxUser('Items/' + firstItem.Id + '/Intros', { + return JellyfinApi.authAjaxUser(`Items/${firstItem.Id}/Intros`, { dataType: 'json', type: 'GET' }); @@ -675,11 +709,11 @@ export function getUser(): Promise { * by resolving things like folders to playable items. * * - * @param userId userId to use - * @param items items to resolve - * @param smart If enabled it will try to find the next episode given the - * current one, if the connected user has enabled that in their settings - * @returns {Promise} Promise for search result containing items to play + * @param userId - userId to use + * @param items - items to resolve + * @param smart - If enabled it will try to find the next episode given the current one, + * if the connected user has enabled that in their settings + * @returns Promise for search result containing items to play */ export async function translateRequestedItems( userId: string, @@ -696,25 +730,25 @@ export async function translateRequestedItems( return await getItemsForPlayback(userId, { ArtistIds: firstItem.Id, Filters: 'IsNotFolder', + MediaTypes: 'Audio', Recursive: true, - SortBy: 'SortName', - MediaTypes: 'Audio' + SortBy: 'SortName' }); } else if (firstItem.Type == 'MusicGenre') { return await getItemsForPlayback(userId, { - Genres: firstItem.Name ?? undefined, Filters: 'IsNotFolder', + Genres: firstItem.Name ?? undefined, + MediaTypes: 'Audio', Recursive: true, - SortBy: 'SortName', - MediaTypes: 'Audio' + SortBy: 'SortName' }); } else if (firstItem.IsFolder) { return await getItemsForPlayback(userId, { - ParentId: firstItem.Id, Filters: 'IsNotFolder', + MediaTypes: 'Audio,Video', + ParentId: firstItem.Id, Recursive: true, - SortBy: 'SortName', - MediaTypes: 'Audio,Video' + SortBy: 'SortName' }); } else if (smart && firstItem.Type == 'Episode' && items.length == 1) { const user = await getUser(); @@ -729,7 +763,9 @@ export async function translateRequestedItems( Ids: firstItem.Id }); - if (!result.Items || result.Items.length < 1) return result; + if (!result.Items || result.Items.length < 1) { + return result; + } const episode = result.Items[0]; @@ -741,26 +777,29 @@ export async function translateRequestedItems( userId, episode.SeriesId, { - IsVirtualUnaired: false, IsMissing: false, + IsVirtualUnaired: false, UserId: userId } ); let foundItem = false; - episodesResult.Items = episodesResult.Items?.filter(function ( - e: BaseItemDto - ) { - if (foundItem) { - return true; - } - if (e.Id == episode.Id) { - foundItem = true; - return true; - } - return false; - }); + episodesResult.Items = episodesResult.Items?.filter( + (e: BaseItemDto) => { + if (foundItem) { + return true; + } + + if (e.Id == episode.Id) { + foundItem = true; + + return true; + } + + return false; + } + ); episodesResult.TotalRecordCount = episodesResult.Items?.length || 0; @@ -777,14 +816,15 @@ export async function translateRequestedItems( * * TODO can we remove this crap * - * @param target object that gets populated with entries - * @param source object that the entries are copied from - * @returns {any} reference to target object + * @param target - object that gets populated with entries + * @param source - object that the entries are copied from + * @returns reference to target object */ export function extend(target: any, source: any): any { for (const i in source) { target[i] = source[i]; } + return target; } @@ -793,7 +833,7 @@ export function extend(target: any, source: any): any { * but could be useful to deal with weird date strings * in the future. * - * @param date string date to parse + * @param date - string date to parse * @returns date object */ export function parseISO8601Date(date: string): Date { @@ -803,7 +843,7 @@ export function parseISO8601Date(date: string): Date { /** * Send a message over the custom message transport * - * @param message to send + * @param message - to send */ export function broadcastToMessageBus(message: BusMessage): void { window.castReceiverContext.sendCustomMessage( @@ -817,13 +857,13 @@ export function broadcastToMessageBus(message: BusMessage): void { * Inform the cast sender that we couldn't connect */ export function broadcastConnectionErrorMessage(): void { - broadcastToMessageBus({ type: 'connectionerror', message: '' }); + broadcastToMessageBus({ message: '', type: 'connectionerror' }); } /** * Remove all special characters from a string * - * @param name input string + * @param name - input string * @returns string with non-whitespace non-word characters removed */ export function cleanName(name: string): string { diff --git a/src/types/global.d.ts b/src/types/global.d.ts index ea259f5f..2018cfb8 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -1,4 +1,3 @@ -import { cast } from 'chromecast-caf-receiver'; import { CastReceiverContext, PlayerManager @@ -25,6 +24,7 @@ export interface Dictionary { // Why doesn't the API have a type for this? /* Combined item query. * Valid for item endpoints */ +// TODO: API has an endpoint for this. Replace on https://github.com/jellyfin/jellyfin-chromecast/pull/109 export interface ItemQuery { UserId?: string; Limit?: number; diff --git a/stylelint.config.js b/stylelint.config.js new file mode 100644 index 00000000..73562f86 --- /dev/null +++ b/stylelint.config.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['stylelint-config-standard', 'stylelint-config-prettier'], + rules: { + 'at-rule-no-unknown': null + }, + syntax: 'css' +}; diff --git a/tsconfig.json b/tsconfig.json index 61f2c12e..a8aa9c3c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,9 +14,6 @@ "paths": { "~/*": ["./*"] }, - "types": [ - "@types/node", - "@types/chromecast-caf-receiver" - ] + "types": ["@types/node", "@types/chromecast-caf-receiver"] } } diff --git a/webpack.config.ts b/webpack.config.ts index 9010da6c..8da42000 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -7,27 +7,40 @@ import { merge } from 'webpack-merge'; import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import ImageMinimizerPlugin from 'image-minimizer-webpack-plugin'; -import version from './package.json'; +import { version } from './package.json'; const common: webpack.Configuration = { context: path.resolve(__dirname, 'src'), entry: './app.ts', + module: { + rules: [ + { loader: 'html-loader', test: /\.html$/ }, + { + test: /\.(png|svg|jpg|gif)$/, + use: 'file-loader' + }, + { + loader: 'file-loader', + test: /\.(ttf|eot|woff(2)?)(\?[a-z0-9=&.]+)?$/ + }, + { test: /\.css$/i, use: ['style-loader', 'css-loader'] }, + { loader: 'ts-loader', test: /\.tsx?$/ }, + { loader: 'source-map-loader', test: /\.js$/ } + ] + }, output: { filename: '[name].[fullhash].js', path: path.resolve(__dirname, 'dist'), publicPath: './' }, - resolve: { - extensions: ['.ts', '.js'] - }, plugins: [ // @ts-expect-error - Typings mismatch between versions new CleanWebpackPlugin(), new HtmlWebpackPlugin({ + favicon: 'favicon.ico', filename: 'index.html', - template: 'index.html', hash: false, - favicon: 'favicon.ico' + template: 'index.html' }), new ImageMinimizerPlugin({ minimizerOptions: { @@ -46,36 +59,23 @@ const common: webpack.Configuration = { } }) ], - module: { - rules: [ - { test: /\.html$/, loader: 'html-loader' }, - { - test: /\.(png|svg|jpg|gif)$/, - use: 'file-loader' - }, - { - test: /\.(ttf|eot|woff(2)?)(\?[a-z0-9=&.]+)?$/, - loader: 'file-loader' - }, - { test: /\.css$/i, use: ['style-loader', 'css-loader'] }, - { test: /\.tsx?$/, loader: 'ts-loader' }, - { test: /\.js$/, loader: 'source-map-loader' } - ] + resolve: { + extensions: ['.ts', '.js'] } }; const development: webpack.Configuration = { - mode: 'development', - devtool: 'inline-source-map', // @ts-expect-error - Typings mismatch between versions devServer: { - contentBase: path.join(__dirname, 'dist'), compress: true, + contentBase: path.join(__dirname, 'dist'), port: process.env.RECEIVER_PORT ? Number.parseInt(process.env.RECEIVER_PORT, 10) : 9000, publicPath: '/' }, + devtool: 'inline-source-map', + mode: 'development', plugins: [ new DefinePlugin({ PRODUCTION: JSON.stringify(false), @@ -94,10 +94,9 @@ const production: webpack.Configuration = { ] }; -module.exports = ( - argv: { [key: string]: string } -): webpack.Configuration => { +module.exports = (argv: { [key: string]: string }): webpack.Configuration => { let config; + if (argv.mode === 'production') { config = merge(common, production); } else {