diff --git a/.eslintrc.js b/.eslintrc.js index 2fae10f..12c3607 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,6 +7,8 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', ], overrides: [ { @@ -24,6 +26,42 @@ module.exports = { ecmaVersion: 'latest', sourceType: 'module', }, - plugins: ['@typescript-eslint'], - rules: {}, + plugins: ['@typescript-eslint', 'import'], + rules: { + 'import/order': [ + 'error', + { + groups: [ + 'builtin', + 'external', + 'internal', + 'sibling', + 'parent', + 'object', + 'type', + ], + pathGroups: [ + { + pattern: '@/**', + group: 'internal', + }, + { + pattern: '@test/**', + group: 'internal', + }, + ], + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + 'newlines-between': 'always', + }, + ], + }, + settings: { + 'import/resolver': { + typescript: true, + node: true, + }, + }, }; diff --git a/.prettierrc.json b/.prettierrc.json index 92cde39..544138b 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,3 +1,3 @@ { "singleQuote": true -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index d1b4edb..a860ca7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,13 @@ { "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode" -} \ No newline at end of file + "editor.defaultFormatter": "esbenp.prettier-vscode", + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } +} diff --git a/package.json b/package.json index 0d2382d..6f6298e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tabnews-sdk", "version": "1.0.0", - "description": "Node.js library to interacte with TabNews API", + "description": "Node.js library to interact with TabNews API", "main": "build/src/index.js", "typings": "build/src/index.d.ts", "files": [ @@ -44,6 +44,8 @@ "cz-conventional-changelog": "^3.3.0", "eslint": "^8.49.0", "eslint-config-prettier": "^9.0.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", "eslint-plugin-prettier": "^5.0.0", "husky": "^8.0.3", "lint-staged": "^14.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25e3e4b..6e0b6ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,6 +40,12 @@ devDependencies: eslint-config-prettier: specifier: ^9.0.0 version: 9.0.0(eslint@8.49.0) + eslint-import-resolver-typescript: + specifier: ^3.6.1 + version: 3.6.1(@typescript-eslint/parser@6.7.0)(eslint-plugin-import@2.28.1)(eslint@8.49.0) + eslint-plugin-import: + specifier: ^2.28.1 + version: 2.28.1(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.49.0) eslint-plugin-prettier: specifier: ^5.0.0 version: 5.0.0(eslint-config-prettier@9.0.0)(eslint@8.49.0)(prettier@3.0.3) @@ -664,6 +670,10 @@ packages: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: true + /@types/minimist@1.2.2: resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} dev: true @@ -977,15 +987,77 @@ packages: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: true + /array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} dev: true + /array-includes@3.1.7: + resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + is-string: 1.0.7 + dev: true + /array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} dev: true + /array.prototype.findlastindex@1.2.3: + resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + get-intrinsic: 1.2.1 + dev: true + + /array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + es-shim-unscopables: 1.0.0 + dev: true + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: true + /arrify@1.0.1: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} @@ -1000,6 +1072,11 @@ packages: engines: {node: '>= 4.0.0'} dev: true + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true @@ -1066,6 +1143,13 @@ packages: engines: {node: '>=6'} dev: true + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: true + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1357,6 +1441,17 @@ packages: engines: {node: '>=8'} dev: true + /debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -1421,11 +1516,29 @@ packages: clone: 1.0.4 dev: true + /define-data-property@1.1.0: + resolution: {integrity: sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + dev: true + /define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} dev: true + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + /detect-file@1.0.0: resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} engines: {node: '>=0.10.0'} @@ -1454,6 +1567,13 @@ packages: path-type: 4.0.0 dev: true + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + /doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -1480,6 +1600,14 @@ packages: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: true + /enhanced-resolve@5.15.0: + resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + dev: true + /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} requiresBuild: true @@ -1487,6 +1615,75 @@ packages: is-arrayish: 0.2.1 dev: true + /es-abstract@1.22.2: + resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.11 + dev: true + + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: true + + /es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + /esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} @@ -1541,6 +1738,104 @@ packages: eslint: 8.49.0 dev: true + /eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + dependencies: + debug: 3.2.7 + is-core-module: 2.13.0 + resolve: 1.22.4 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.7.0)(eslint-plugin-import@2.28.1)(eslint@8.49.0): + resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + dependencies: + debug: 4.3.4 + enhanced-resolve: 5.15.0 + eslint: 8.49.0 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.49.0) + eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.49.0) + fast-glob: 3.3.1 + get-tsconfig: 4.7.2 + is-core-module: 2.13.0 + is-glob: 4.0.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.49.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.7.0(eslint@8.49.0)(typescript@5.2.2) + debug: 3.2.7 + eslint: 8.49.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.7.0)(eslint-plugin-import@2.28.1)(eslint@8.49.0) + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.49.0): + resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 6.7.0(eslint@8.49.0)(typescript@5.2.2) + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.49.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.49.0) + has: 1.0.3 + is-core-module: 2.13.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-plugin-prettier@5.0.0(eslint-config-prettier@9.0.0)(eslint@8.49.0)(prettier@3.0.3): resolution: {integrity: sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1808,6 +2103,12 @@ packages: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} dev: true + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + /fs-extra@11.1.1: resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} engines: {node: '>=14.14'} @@ -1843,6 +2144,20 @@ packages: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} dev: true + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1852,11 +2167,34 @@ packages: resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} dev: true + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: true + /get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} dev: true + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: true + + /get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + /git-raw-commits@2.0.11: resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} engines: {node: '>=10'} @@ -1928,6 +2266,13 @@ packages: type-fest: 0.20.2 dev: true + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: true + /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -1940,6 +2285,12 @@ packages: slash: 3.0.0 dev: true + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true @@ -1953,6 +2304,10 @@ packages: engines: {node: '>=6'} dev: true + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -1963,6 +2318,29 @@ packages: engines: {node: '>=8'} dev: true + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} @@ -2078,17 +2456,60 @@ packages: wrap-ansi: 7.0.0 dev: true + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + side-channel: 1.0.4 + dev: true + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: true + /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} requiresBuild: true dev: true + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + /is-core-module@2.13.0: resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} dependencies: has: 1.0.3 dev: true + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + /is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -2136,6 +2557,18 @@ packages: engines: {node: '>=8'} dev: true + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -2156,6 +2589,20 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: true + /is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -2166,6 +2613,20 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + /is-text-path@1.0.1: resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==} engines: {node: '>=0.10.0'} @@ -2173,6 +2634,13 @@ packages: text-extensions: 1.9.0 dev: true + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.11 + dev: true + /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -2182,6 +2650,12 @@ packages: resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==} dev: true + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: true + /is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -2194,6 +2668,10 @@ packages: is-docker: 2.2.1 dev: true + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true @@ -2265,6 +2743,13 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true + /jsonc-parser@3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} dev: true @@ -2631,6 +3116,52 @@ packages: path-key: 4.0.0 dev: true + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.fromentries@2.0.7: + resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /object.groupby@1.0.1: + resolution: {integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + get-intrinsic: 1.2.1 + dev: true + + /object.values@1.1.7: + resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -2905,6 +3436,15 @@ packages: strip-indent: 3.0.0 dev: true + /regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + set-function-name: 2.0.1 + dev: true + /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -2941,6 +3481,10 @@ packages: global-dirs: 0.1.1 dev: true + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + /resolve@1.22.4: resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} hasBin: true @@ -3014,10 +3558,28 @@ packages: tslib: 2.6.2 dev: true + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} dev: true + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + dev: true + /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: true @@ -3027,6 +3589,11 @@ packages: hasBin: true dev: true + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + /semver@7.5.4: resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} engines: {node: '>=10'} @@ -3035,6 +3602,15 @@ packages: lru-cache: 6.0.0 dev: true + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.0 + dev: true + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3047,6 +3623,14 @@ packages: engines: {node: '>=8'} dev: true + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + dev: true + /siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} dev: true @@ -3137,6 +3721,31 @@ packages: strip-ansi: 7.1.0 dev: true + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.2 + dev: true + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -3157,6 +3766,11 @@ packages: ansi-regex: 6.0.1 dev: true + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + /strip-bom@4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} @@ -3217,6 +3831,11 @@ packages: tslib: 2.6.2 dev: true + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + dev: true + /test-exclude@6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -3327,6 +3946,15 @@ packages: yn: 3.1.1 dev: true + /tsconfig-paths@3.14.2: + resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} dev: true @@ -3373,6 +4001,44 @@ packages: engines: {node: '>=10'} dev: true + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: true + /typescript@5.2.2: resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} engines: {node: '>=14.17'} @@ -3383,6 +4049,15 @@ packages: resolution: {integrity: sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==} dev: true + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + /universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} @@ -3574,6 +4249,27 @@ packages: tr46: 0.0.3 webidl-conversions: 3.0.1 + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true diff --git a/src/commons/constants.ts b/src/commons/constants.ts new file mode 100644 index 0000000..b7a0f71 --- /dev/null +++ b/src/commons/constants.ts @@ -0,0 +1,15 @@ +export const TABNEWS_BASE_URL = + process.env.TABNEWS_BASE_URL || 'https://www.tabnews.com.br/api/v1'; + +export const TABNEWS_ENDPOINTS = Object.freeze({ + session: '/sessions', + user: '/user', + content: '/contents', +}); + +export const TABNEWS_HEADERS = Object.freeze({ + cookie: 'Cookie', + link: 'link', + paginationTotalRows: 'x-pagination-total-rows', + sessionId: 'session_id', +}); diff --git a/src/commons/errors.ts b/src/commons/errors.ts index 15f13e8..8f8d9e1 100644 --- a/src/commons/errors.ts +++ b/src/commons/errors.ts @@ -2,28 +2,28 @@ import { TabNewsApiError } from './interfaces'; export class TabNewsError extends Error implements TabNewsApiError { action: string; - statusCode: number; - errorId?: string; - requestId?: string; + status_code: number; + error_id?: string; + request_id?: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any context?: any; - errorLocationCode?: string; + error_location_code?: string; key?: string; type?: string; - databaseErrorCode?: string; + database_error_code?: string; - constructor(readonly tabNewsError: TabNewsApiError) { + constructor(tabNewsError: TabNewsApiError) { super(tabNewsError.message); this.name = tabNewsError.name; this.action = tabNewsError.action; - this.statusCode = tabNewsError.statusCode; - this.errorId = tabNewsError.errorId; - this.requestId = tabNewsError.requestId; + this.status_code = tabNewsError.status_code; + this.error_id = tabNewsError.error_id; + this.request_id = tabNewsError.request_id; this.context = tabNewsError.context; this.stack = tabNewsError.stack; - this.errorLocationCode = tabNewsError.errorLocationCode; + this.error_location_code = tabNewsError.error_location_code; this.key = tabNewsError.key; this.type = tabNewsError.type; - this.databaseErrorCode = tabNewsError.databaseErrorCode; + this.database_error_code = tabNewsError.database_error_code; } } diff --git a/src/commons/index.ts b/src/commons/index.ts new file mode 100644 index 0000000..ab27c69 --- /dev/null +++ b/src/commons/index.ts @@ -0,0 +1,3 @@ +export * from './errors'; +export * from './interfaces'; +export * from './constants'; diff --git a/src/commons/interfaces.ts b/src/commons/interfaces.ts index 8d92069..fed55ee 100644 --- a/src/commons/interfaces.ts +++ b/src/commons/interfaces.ts @@ -2,14 +2,14 @@ export interface TabNewsApiError { name: string; message: string; action: string; - statusCode: number; + status_code: number; stack?: string; - errorId?: string; - requestId?: string; + error_id?: string; + request_id?: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any context?: any; - errorLocationCode?: string; + error_location_code?: string; key?: string; type?: string; - databaseErrorCode?: string; + database_error_code?: string; } diff --git a/src/content/__snapshots__/content.spec.ts.snap b/src/content/__snapshots__/content.spec.ts.snap new file mode 100644 index 0000000..c6925af --- /dev/null +++ b/src/content/__snapshots__/content.spec.ts.snap @@ -0,0 +1,92 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Content > create > should create content 1`] = ` +{ + "body": "test", + "created_at": 2023-09-19T10:29:20.967Z, + "deleted_at": null, + "id": "502d02a1-2e38-4cc6-9e41-55c978e34c67", + "owner_id": "605144ec-9f0d-437b-96ee-1b5f4e54f5f9", + "owner_username": "leoferreiralima", + "parent_id": null, + "published_at": null, + "slug": "e-opcional", + "source_url": null, + "status": "draft", + "tabcoins": 0, + "title": "test", + "updated_at": 2023-09-19T10:29:20.967Z, +} +`; + +exports[`Content > create > should throw an error when create content with invalid parameters 1`] = `"\\"body\\" possui o valor inválido \\"null\\"."`; + +exports[`Content > get > should return all contents and pagination 1`] = ` +{ + "first_page": 1, + "last_page": 6, + "next_page": 2, + "page": 1, + "per_page": 30, + "previous_page": undefined, + "strategy": "relevant", + "total_rows": 175, +} +`; + +exports[`Content > get > should return all contents and pagination 2`] = ` +[ + { + "children_deep_count": 2, + "created_at": 2023-09-19T12:16:04.812Z, + "deleted_at": null, + "id": "id", + "owner_id": "owner_id", + "owner_username": "username", + "parent_id": null, + "published_at": 2023-09-19T12:16:04.837Z, + "slug": "slug", + "source_url": "https://source.url.com/source", + "status": "published", + "tabcoins": 1, + "title": "title", + "updated_at": 2023-09-19T12:16:04.812Z, + }, +] +`; + +exports[`Content > get > should return correct page when is last page 1`] = ` +{ + "first_page": 1, + "last_page": 6, + "next_page": undefined, + "page": 6, + "per_page": 30, + "previous_page": 5, + "strategy": "relevant", + "total_rows": 175, +} +`; + +exports[`Content > get > should return correct page when is last page 2`] = ` +[ + { + "children_deep_count": 2, + "created_at": 2023-09-19T12:16:04.812Z, + "deleted_at": null, + "id": "id", + "owner_id": "owner_id", + "owner_username": "username", + "parent_id": null, + "published_at": 2023-09-19T12:16:04.837Z, + "slug": "slug", + "source_url": "https://source.url.com/source", + "status": "published", + "tabcoins": 1, + "title": "title", + "updated_at": 2023-09-19T12:16:04.812Z, + }, +] +`; + +exports[`Content > get > should return error when parameter is invalid 1`] = `"\\"page\\" deve possuir um valor mínimo de 1."`; diff --git a/src/content/content.spec.ts b/src/content/content.spec.ts index 113dcc0..6756e46 100644 --- a/src/content/content.spec.ts +++ b/src/content/content.spec.ts @@ -1,7 +1,18 @@ -import 'vitest-fetch-mock'; - import { expect, describe, it, afterEach, beforeEach } from 'vitest'; -import { TabNews } from '../tabnews'; + +import { TABNEWS_ENDPOINTS, TABNEWS_HEADERS } from '@/commons'; +import { TabNews } from '@/tabnews'; +import { + createTabNews, + expectRequest, + mockOnceApiError, + mockOnceResponse, + mockOnceSession, + mockedRequest, + mockedRequests, + resetMocks, +} from '@test/utils'; + import { GetContentParams } from './interfaces'; let tabNews: TabNews; @@ -16,144 +27,70 @@ const lastPageLinkHeader = '; rel="prev", ' + '; rel="last"'; +const content = { + id: 'id', + owner_id: 'owner_id', + parent_id: null, + slug: 'slug', + title: 'title', + status: 'published', + source_url: 'https://source.url.com/source', + created_at: '2023-09-19T12:16:04.812Z', + updated_at: '2023-09-19T12:16:04.812Z', + published_at: '2023-09-19T12:16:04.837Z', + deleted_at: null, + tabcoins: 1, + owner_username: 'username', + children_deep_count: 2, +}; + describe('Content', () => { beforeEach(() => { - tabNews = new TabNews({ - credentials: { - email: 'test@email.com', - password: 'dummy_password', - }, - }); + tabNews = createTabNews(); }); afterEach(() => { - fetchMock.resetMocks(); + resetMocks(); }); describe('get', () => { - it('should return all contents and paginagiton', async () => { - fetchMock.mockOnce( - JSON.stringify([ - { - id: 'id', - owner_id: 'owner_id', - parent_id: null, - slug: 'slug', - title: 'title', - status: 'published', - source_url: 'https://source.url.com/source', - created_at: '2023-09-19T12:16:04.812Z', - updated_at: '2023-09-19T12:16:04.812Z', - published_at: '2023-09-19T12:16:04.837Z', - deleted_at: null, - tabcoins: 1, - owner_username: 'username', - children_deep_count: 2, - }, - ]), - { - headers: { - link: linkHeader, - 'x-pagination-total-rows': '175', - }, + const mockContents = (link: string) => { + mockOnceResponse(TABNEWS_ENDPOINTS.content, [content], { + headers: { + [TABNEWS_HEADERS.link]: link, + [TABNEWS_HEADERS.paginationTotalRows]: '175', }, - ); + }); + }; + + it('should return all contents and pagination', async () => { + mockContents(linkHeader); const response = await tabNews.content.getAll(); - expect(response.pagination).toMatchObject({ - strategy: 'relevant', - page: 1, - per_page: 30, - first_page: 1, - last_page: 6, - next_page: 2, - previous_page: undefined, - total_rows: 175, - }); + expect(response.pagination).toMatchSnapshot(); + expect(response.contents).toMatchSnapshot(); - expect(response.contents).toMatchObject([ - { - id: 'id', - owner_id: 'owner_id', - parent_id: null, - slug: 'slug', - title: 'title', - status: 'published', - source_url: 'https://source.url.com/source', - created_at: new Date('2023-09-19T12:16:04.812Z'), - updated_at: new Date('2023-09-19T12:16:04.812Z'), - published_at: new Date('2023-09-19T12:16:04.837Z'), - deleted_at: null, - tabcoins: 1, - owner_username: 'username', - children_deep_count: 2, - }, - ]); + const request = mockedRequest(); + + expectRequest(request).method.toBeGet(); }); it('should return correct page when is last page', async () => { - fetchMock.mockOnce( - JSON.stringify([ - { - id: 'id', - owner_id: 'owner_id', - parent_id: null, - slug: 'slug', - title: 'title', - status: 'published', - source_url: 'https://source.url.com/source', - created_at: '2023-09-19T12:16:04.812Z', - updated_at: '2023-09-19T12:16:04.812Z', - published_at: '2023-09-19T12:16:04.837Z', - deleted_at: null, - tabcoins: 1, - owner_username: 'username', - children_deep_count: 2, - }, - ]), - { - headers: { - link: lastPageLinkHeader, - 'x-pagination-total-rows': '175', - }, - }, - ); + mockContents(lastPageLinkHeader); const response = await tabNews.content.getAll(); - expect(response.pagination).toMatchObject({ - strategy: 'relevant', - page: 6, - per_page: 30, - first_page: 1, - last_page: 6, - next_page: undefined, - previous_page: 5, - total_rows: 175, - }); + expect(response.pagination).toMatchSnapshot(); - expect(response.contents).toMatchObject([ - { - id: 'id', - owner_id: 'owner_id', - parent_id: null, - slug: 'slug', - title: 'title', - status: 'published', - source_url: 'https://source.url.com/source', - created_at: new Date('2023-09-19T12:16:04.812Z'), - updated_at: new Date('2023-09-19T12:16:04.812Z'), - published_at: new Date('2023-09-19T12:16:04.837Z'), - deleted_at: null, - tabcoins: 1, - owner_username: 'username', - children_deep_count: 2, - }, - ]); + expect(response.contents).toMatchSnapshot(); + + const request = mockedRequest(); + + expectRequest(request).method.toBeGet(); }); - it('should send all conteny params correctly', () => { + it('should send all content params correctly', async () => { const paramsList: GetContentParams[] = [ { page: 2, @@ -169,71 +106,75 @@ describe('Content', () => { }, ]; - paramsList.forEach(async (params) => { - fetchMock.mockOnce( - JSON.stringify([ - { - id: 'id', - owner_id: 'owner_id', - parent_id: null, - slug: 'slug', - title: 'title', - status: 'published', - source_url: 'https://source.url.com/source', - created_at: '2023-09-19T12:16:04.812Z', - updated_at: '2023-09-19T12:16:04.812Z', - published_at: '2023-09-19T12:16:04.837Z', - deleted_at: null, - tabcoins: 1, - owner_username: 'username', - children_deep_count: 2, - }, - ]), - { - headers: { - link: linkHeader, - 'x-pagination-total-rows': '175', - }, - }, - ); + for (const params of paramsList) { + mockContents(linkHeader); await tabNews.content.getAll(params); - }); + } + const [first, second, third] = mockedRequests(); + + expectRequest(first).query('page').toBe('2'); + expectRequest(first).query('per_page').toBe('10'); + expectRequest(first).query('strategy').toBe('new'); - const requests = fetchMock.requests(); + expectRequest(second).query('page').toBeNull(); + expectRequest(second).query('per_page').toBe('20'); + expectRequest(second).query('strategy').toBe('new'); - expect( - requests[0].url.endsWith('page=2&per_page=10&strategy=new'), - ).toBeTruthy(); + expectRequest(third).query('page').toBeNull(); + expectRequest(third).query('per_page').toBeNull(); + expectRequest(third).query('strategy').toBe('old'); + }); + + it('should return error when parameter is invalid', () => { + mockOnceApiError(TABNEWS_ENDPOINTS.content, { + name: 'ValidationError', + message: '"page" deve possuir um valor mínimo de 1.', + action: 'Ajuste os dados enviados e tente novamente.', + status_code: 400, + error_id: 'd2ae3240-21db-45fe-9985-814aa317dfa1', + request_id: '4b0a7574-9b89-41f8-8e52-97b972958a67', + error_location_code: 'MODEL:VALIDATOR:FINAL_SCHEMA', + key: 'page', + type: 'number.min', + }); - expect(requests[1].url.endsWith('per_page=20&strategy=new')).toBeTruthy(); - expect(requests[2].url.endsWith('strategy=old')).toBeTruthy(); + expect(() => + tabNews.content.getAll({ + page: 0, + }), + ).rejects.toThrowErrorMatchingSnapshot(); }); }); describe('create', () => { + const mockCreateContent = () => { + mockOnceResponse(TABNEWS_ENDPOINTS.content, { + id: '502d02a1-2e38-4cc6-9e41-55c978e34c67', + owner_id: '605144ec-9f0d-437b-96ee-1b5f4e54f5f9', + parent_id: null, + slug: 'e-opcional', + title: 'test', + body: 'test', + status: 'draft', + source_url: null, + created_at: '2023-09-19T10:29:20.967Z', + updated_at: '2023-09-19T10:29:20.967Z', + published_at: null, + deleted_at: null, + owner_username: 'leoferreiralima', + tabcoins: 0, + }); + }; + it('should create content', async () => { - fetchMock.mockOnce( - JSON.stringify({ - id: '502d02a1-2e38-4cc6-9e41-55c978e34c67', - owner_id: '605144ec-9f0d-437b-96ee-1b5f4e54f5f9', - parent_id: null, - slug: 'e-opcional', - title: 'test', - body: 'test', - status: 'draft', - source_url: null, - created_at: '2023-09-19T10:29:20.967Z', - updated_at: '2023-09-19T10:29:20.967Z', - published_at: null, - deleted_at: null, - owner_username: 'leoferreiralima', - tabcoins: 0, - }), - ); + mockOnceSession(); + + mockCreateContent(); + + await tabNews.session.create(); const response = await tabNews.content.create({ - parent_id: undefined, slug: 'e-opcional', title: 'test', body: 'test', @@ -241,22 +182,44 @@ describe('Content', () => { source_url: 'https://google.com', }); - expect(response).toMatchObject({ - id: '502d02a1-2e38-4cc6-9e41-55c978e34c67', - owner_id: '605144ec-9f0d-437b-96ee-1b5f4e54f5f9', - parent_id: null, + expect(response).toMatchSnapshot(); + + const request = mockedRequest(); + + expectRequest(request).method.toBePost(); + expectRequest(request).body.toBe({ slug: 'e-opcional', title: 'test', body: 'test', status: 'draft', - source_url: null, - created_at: new Date('2023-09-19T10:29:20.967Z'), - updated_at: new Date('2023-09-19T10:29:20.967Z'), - published_at: null, - deleted_at: null, - owner_username: 'leoferreiralima', - tabcoins: 0, + source_url: 'https://google.com', + }); + + expectRequest(request).cookie(TABNEWS_HEADERS.sessionId).toBeDefined(); + }); + + it('should throw an error when create content with invalid parameters', () => { + mockOnceApiError(TABNEWS_ENDPOINTS.content, { + name: 'ValidationError', + message: '"body" possui o valor inválido "null".', + action: 'Ajuste os dados enviados e tente novamente.', + status_code: 400, + error_id: '68a48842-e7ec-4aa8-9549-eb69588cbfe2', + request_id: '6d4278ca-f9e9-4187-a977-f5aab3f11200', + error_location_code: 'MODEL:VALIDATOR:FINAL_SCHEMA', + key: 'body', + type: 'any.invalid', }); + + expect(() => + tabNews.content.create({ + slug: 'e-opcional', + title: 'test', + body: 'invalid', + status: 'draft', + source_url: 'https://google.com', + }), + ).rejects.toThrowErrorMatchingSnapshot(); }); }); }); diff --git a/src/content/content.ts b/src/content/content.ts index e288972..e484e22 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -1,5 +1,7 @@ -import { TabNews } from '../tabnews'; -import { parseLink } from '../utils'; +import { TABNEWS_ENDPOINTS } from '@/commons'; +import { TabNews } from '@/tabnews'; +import { parseLink } from '@/utils'; + import { ContentPagination, ContentResponse, @@ -14,7 +16,7 @@ export class Content { async create(contentBody: CreateContent) { const { body: content } = await this.tabNews.post({ - path: '/contents', + path: TABNEWS_ENDPOINTS.content, body: contentBody, }); @@ -34,7 +36,9 @@ export class Content { ContentResponse[] >({ path: - urlParams.size > 0 ? `/contents?${urlParams.toString()}` : '/contents', + urlParams.size > 0 + ? `${TABNEWS_ENDPOINTS.content}?${urlParams.toString()}` + : TABNEWS_ENDPOINTS.content, }); const links = parseLink(headers.get('link')!); diff --git a/src/fetch/fetch.spec.ts b/src/fetch/fetch.spec.ts index 76570f9..afadb11 100644 --- a/src/fetch/fetch.spec.ts +++ b/src/fetch/fetch.spec.ts @@ -1,6 +1,7 @@ import 'vitest-fetch-mock'; import { expect, describe, it } from 'vitest'; + import { fetch } from './fetch'; describe('fetch', () => { diff --git a/src/session/__snapshots__/session.spec.ts.snap b/src/session/__snapshots__/session.spec.ts.snap index b49bf48..44c0a30 100644 --- a/src/session/__snapshots__/session.spec.ts.snap +++ b/src/session/__snapshots__/session.spec.ts.snap @@ -1,3 +1,24 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Session > create > should throw a tabnews error when api return error 1`] = `"\\"user\\" não possui \\"features\\" ou não é um array."`; +exports[`Session > create > should create a session 1`] = ` +{ + "created_at": 2023-09-12T11:56:13.379Z, + "expires_at": 2023-10-12T11:56:13.378Z, + "id": "123", + "token": "token123", + "updated_at": 2023-09-12T11:56:13.379Z, +} +`; + +exports[`Session > create > should throw a tabnews error when api return error 1`] = `"Dados não conferem."`; + +exports[`Session > destroy > should destroy a session 1`] = ` +{ + "created_at": 2023-09-12T11:56:13.379Z, + "expires_at": 2023-10-12T11:56:13.378Z, + "id": "123", + "updated_at": 2023-09-12T11:56:13.379Z, +} +`; + +exports[`Session > destroy > should throw an error when has no session 1`] = `"Usuário não pode executar esta operação."`; diff --git a/src/session/session.spec.ts b/src/session/session.spec.ts index 39a7dad..eae53dd 100644 --- a/src/session/session.spec.ts +++ b/src/session/session.spec.ts @@ -1,56 +1,67 @@ import 'vitest-fetch-mock'; import { expect, describe, it, afterEach, beforeEach, vi } from 'vitest'; -import { TabNews } from '../tabnews'; + +import { TABNEWS_ENDPOINTS, TABNEWS_HEADERS } from '@/commons'; +import { TabNews } from '@/tabnews'; +import { + DEFAULT_TABNEWS_CONFIG, + createTabNews, + expectRequest, + mockOnceApiError, + mockOnceResponse, + mockOnceSession, + mockedRequest, + resetMocks, +} from '@test/utils'; let tabNews: TabNews; describe('Session', () => { - beforeEach(() => { - tabNews = new TabNews({ - credentials: { - email: 'test@email.com', - password: 'dummy_password', - }, + const mockDestroySession = () => + mockOnceResponse(TABNEWS_ENDPOINTS.session, { + id: '123', + expires_at: '2023-10-12T11:56:13.378Z', + created_at: '2023-09-12T11:56:13.379Z', + updated_at: '2023-09-12T11:56:13.379Z', }); + + beforeEach(() => { + tabNews = createTabNews(); }); + afterEach(() => { - fetchMock.resetMocks(); + resetMocks(); }); describe('create', () => { it('should create a session', async () => { - fetchMock.mockOnce( - JSON.stringify({ - id: '123', - token: 'token123', - expires_at: '2023-10-12T11:56:13.378Z', - created_at: '2023-09-12T11:56:13.379Z', - updated_at: '2023-09-12T11:56:13.379Z', - }), - ); + mockOnceSession(); + + const session = await tabNews.session.create(); + + expect(session).toMatchSnapshot(); - const data = await tabNews.session.create(); + const request = mockedRequest(); - expect(data).toMatchObject({ - id: '123', - token: 'token123', - expires_at: new Date('2023-10-12T11:56:13.378Z'), - created_at: new Date('2023-09-12T11:56:13.379Z'), - updated_at: new Date('2023-09-12T11:56:13.379Z'), + expectRequest(request).body.toBe({ + email: DEFAULT_TABNEWS_CONFIG.credentials?.email, + password: DEFAULT_TABNEWS_CONFIG.credentials?.password, }); + + expectRequest(request).method.toBePost(); }); - it('should throw a tabnews error when api return error', async () => { - fetchMock.mockOnce( - JSON.stringify({ - message: `"user" não possui "features" ou não é um array.`, - action: `Contate o suporte informado o campo "errorId".`, - }), - { - status: 400, - }, - ); + it('should throw a tabnews error when api return error', () => { + mockOnceApiError(TABNEWS_ENDPOINTS.session, { + name: 'UnauthorizedError', + message: 'Dados não conferem.', + action: 'Verifique se os dados enviados estão corretos.', + status_code: 401, + error_id: '69b34554-7c09-4772-b0cc-497fcb03222b', + request_id: '6efd0399-622b-4ba9-8703-0d80f58308c2', + error_location_code: 'CONTROLLER:SESSIONS:POST_HANDLER:DATA_MISMATCH', + }); expect(() => tabNews.session.create(), @@ -60,35 +71,44 @@ describe('Session', () => { describe('destroy', () => { it('should destroy a session', async () => { - fetchMock.mockOnce( - JSON.stringify({ - id: '123', - token: 'token123', - expires_at: '2023-10-12T11:56:13.378Z', - created_at: '2023-09-12T11:56:13.379Z', - updated_at: '2023-09-12T11:56:13.379Z', - }), - ); + mockOnceSession(); - await tabNews.session.create(); + const session = await tabNews.session.create(); - fetchMock.mockOnce( - JSON.stringify({ - id: '123', - expires_at: '2023-10-12T11:56:13.378Z', - created_at: '2023-09-12T11:56:13.379Z', - updated_at: '2023-09-12T11:56:13.379Z', - }), - ); + mockDestroySession(); + + const destroyedSession = await tabNews.session.destroy(); + + expect(destroyedSession).toMatchSnapshot(); + + const request = mockedRequest(); - const data = await tabNews.session.destroy(); + expectRequest(request).method.toBeDelete(); + expectRequest(request) + .cookie(TABNEWS_HEADERS.sessionId) + .toBe(session.token); + }); - expect(data).toMatchObject({ - id: '123', - expires_at: new Date('2023-10-12T11:56:13.378Z'), - created_at: new Date('2023-09-12T11:56:13.379Z'), - updated_at: new Date('2023-09-12T11:56:13.379Z'), + it('should throw an error when has no session', async () => { + mockOnceApiError(TABNEWS_ENDPOINTS.session, { + name: 'ForbiddenError', + message: 'Usuário não pode executar esta operação.', + action: 'Verifique se este usuário possui a feature "read:session".', + status_code: 403, + error_id: 'b42a2fd9-d3c5-4aaa-bd3a-b92ee15904b2', + request_id: 'cd96640c-9ac2-4976-ae2d-154a29c967a2', + error_location_code: + 'MODEL:AUTHORIZATION:CAN_REQUEST:FEATURE_NOT_FOUND', }); + + expect(() => + tabNews.session.destroy(), + ).rejects.toThrowErrorMatchingSnapshot(); + + const request = mockedRequest(); + + expectRequest(request).method.toBeDelete(); + expectRequest(request).cookie(TABNEWS_HEADERS.sessionId).toBeUndefined(); }); }); diff --git a/src/session/session.ts b/src/session/session.ts index 548d08b..7da720d 100644 --- a/src/session/session.ts +++ b/src/session/session.ts @@ -1,14 +1,16 @@ -import { TabNews } from '../tabnews'; +import { TABNEWS_ENDPOINTS } from '@/commons'; +import { TabNews } from '@/tabnews'; + import { SessionResponse } from './interfaces'; export class Session { - private session?: SessionResponse; + session?: SessionResponse; constructor(private readonly tabNews: TabNews) {} async create() { const { body } = await this.tabNews.post({ - path: '/sessions', + path: TABNEWS_ENDPOINTS.session, body: { email: this.tabNews.config.credentials?.email, password: this.tabNews.config.credentials?.password, @@ -22,7 +24,7 @@ export class Session { async destroy() { const { body } = await this.tabNews.delete>({ - path: '/sessions', + path: TABNEWS_ENDPOINTS.session, }); this.session = undefined; diff --git a/src/tabnews.spec.ts b/src/tabnews.spec.ts index b477b5a..a83befc 100644 --- a/src/tabnews.spec.ts +++ b/src/tabnews.spec.ts @@ -1,6 +1,8 @@ import 'vitest-fetch-mock'; import { expect, describe, it, vi } from 'vitest'; + +import { TABNEWS_HEADERS } from './commons'; import { TabNews } from './tabnews'; describe('TabNews', () => { @@ -60,7 +62,7 @@ describe('TabNews', () => { await tabNews.fetchRequest('/any_route'); - expect(tabNews.headers.get('Cookie')).toBeNull(); + expect(tabNews.headers.get(TABNEWS_HEADERS.cookie)).toBeNull(); }); it('should not create a new session id when dont has configuration', async () => { @@ -77,7 +79,7 @@ describe('TabNews', () => { await tabNews.fetchRequest('/any_route'); - expect(tabNews.headers.get('Cookie')).toBeNull(); + expect(tabNews.headers.get(TABNEWS_HEADERS.cookie)).toBeNull(); }); it('should not create a new session id when dont has session', async () => { @@ -99,7 +101,7 @@ describe('TabNews', () => { await tabNews.fetchRequest('/any_route'); - expect(tabNews.headers.get('Cookie')).toBeNull(); + expect(tabNews.headers.get(TABNEWS_HEADERS.cookie)).toBeNull(); }); it('should not create a new session id when session in not expired', async () => { @@ -124,7 +126,7 @@ describe('TabNews', () => { await tabNews.fetchRequest('/any_route'); - expect(tabNews.headers.get('Cookie')).toBeNull(); + expect(tabNews.headers.get(TABNEWS_HEADERS.cookie)).toBeNull(); }); it('should not create a new session id when session in not expired', async () => { @@ -149,7 +151,7 @@ describe('TabNews', () => { await tabNews.fetchRequest('/any_route'); - expect(tabNews.headers.get('Cookie')).toBeNull(); + expect(tabNews.headers.get(TABNEWS_HEADERS.cookie)).toBeNull(); }); it('should not create a new session id when call session.create()', async () => { @@ -174,7 +176,7 @@ describe('TabNews', () => { await tabNews.session.create(); - expect(tabNews.headers.get('Cookie')).toBeNull(); + expect(tabNews.headers.get(TABNEWS_HEADERS.cookie)).toBeNull(); }); it('should create a new session id when session is expired', async () => { @@ -206,6 +208,8 @@ describe('TabNews', () => { await tabNews.fetchRequest('/any_route'); - expect(tabNews.headers.get('Cookie')).toBe('session_id=token123'); + expect(tabNews.headers.get(TABNEWS_HEADERS.cookie)).toBe( + `${TABNEWS_HEADERS.sessionId}=token123`, + ); }); }); diff --git a/src/tabnews.ts b/src/tabnews.ts index 0fbb6a1..929bae6 100644 --- a/src/tabnews.ts +++ b/src/tabnews.ts @@ -1,15 +1,17 @@ import { Headers } from 'cross-fetch'; -import fetch, { RequestConfig } from './fetch'; -import { TabNewsConfig } from './interfaces'; -import { Session } from './session'; -import { TabNewsApiError } from './commons/interfaces'; -import { TabNewsError } from './commons/errors'; -import { Content } from './content'; -import { User } from './user'; - -const baseUrl = - process.env.TABNEWS_BASE_URL || 'https://www.tabnews.com.br/api/v1'; +import { + TABNEWS_BASE_URL, + TABNEWS_ENDPOINTS, + TABNEWS_HEADERS, + TabNewsApiError, + TabNewsError, +} from '@/commons'; +import { Content } from '@/content'; +import fetch, { RequestConfig } from '@/fetch'; +import { TabNewsConfig } from '@/interfaces'; +import { Session } from '@/session'; +import { User } from '@/user'; export class TabNews { readonly headers: Headers; @@ -38,20 +40,27 @@ export class TabNews { { body, ...options }: Omit = {}, ) { const isCreateSession = - path.includes('/sessions') && 'POST' === options.method; + path.includes(TABNEWS_ENDPOINTS.session) && 'POST' === options.method; const shouldRenovateSession = !isCreateSession && this.isCredentialsConfigured() && - this.session.hasSession() && - this.session.isExpired(); + this.session.hasSession(); if (shouldRenovateSession) { - const session = await this.session.create(); - this.headers.set('Cookie', `session_id=${session.token}`); + // TODO add a better solution later + const session = this.session.isExpired() + ? await this.session.create() + : this.session.session; + if (session) { + this.headers.set( + TABNEWS_HEADERS.cookie, + `${TABNEWS_HEADERS.sessionId}=${session.token}`, + ); + } } - const response = await fetch(`${baseUrl}${path}`, { + const response = await fetch(`${TABNEWS_BASE_URL}${path}`, { headers: this.headers, body, ...options, diff --git a/src/user/__snapshots__/user.spec.ts.snap b/src/user/__snapshots__/user.spec.ts.snap new file mode 100644 index 0000000..ef1f488 --- /dev/null +++ b/src/user/__snapshots__/user.spec.ts.snap @@ -0,0 +1,26 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`User > me > should get me 1`] = ` +{ + "created_at": 2022-04-05T23:50:19.920Z, + "description": "description", + "email": "my@email.com", + "features": [ + "create:session", + "read:session", + "create:content", + "create:content:text_root", + "create:content:text_child", + "update:content", + "update:user", + ], + "id": "id", + "notifications": true, + "tabcash": 10, + "tabcoins": 9, + "updated_at": 2022-04-05T23:50:56.545Z, + "username": "username", +} +`; + +exports[`User > me > should throw an error when has no session 1`] = `"Usuário não pode executar esta operação."`; diff --git a/src/user/user.spec.ts b/src/user/user.spec.ts index 156c147..1594277 100644 --- a/src/user/user.spec.ts +++ b/src/user/user.spec.ts @@ -1,68 +1,89 @@ import 'vitest-fetch-mock'; -import { expect, describe, it, beforeEach } from 'vitest'; -import { TabNews } from '../tabnews'; +import { expect, describe, it, beforeEach, afterEach } from 'vitest'; + +import { TABNEWS_ENDPOINTS, TABNEWS_HEADERS } from '@/commons'; +import { TabNews } from '@/tabnews'; +import { + createTabNews, + expectRequest, + mockOnceApiError, + mockOnceResponse, + mockOnceSession, + mockedRequest, + resetMocks, +} from '@test/utils'; let tabNews: TabNews; +const user = { + id: 'id', + username: 'username', + email: 'my@email.com', + description: 'description', + notifications: true, + features: [ + 'create:session', + 'read:session', + 'create:content', + 'create:content:text_root', + 'create:content:text_child', + 'update:content', + 'update:user', + ], + tabcoins: 9, + tabcash: 10, + created_at: '2022-04-05T23:50:19.920Z', + updated_at: '2022-04-05T23:50:56.545Z', +}; + describe('User', () => { beforeEach(() => { - tabNews = new TabNews({ - credentials: { - email: 'test@email.com', - password: 'dummy_password', - }, - }); + tabNews = createTabNews(); + }); + + afterEach(() => { + resetMocks(); }); describe('me', () => { it('should get me', async () => { - fetchMock.mockOnceIf( - (request) => request.url.endsWith('/user'), - JSON.stringify({ - id: 'id', - username: 'username', - email: 'my@email.com', - description: 'description', - notifications: true, - features: [ - 'create:session', - 'read:session', - 'create:content', - 'create:content:text_root', - 'create:content:text_child', - 'update:content', - 'update:user', - ], - tabcoins: 9, - tabcash: 10, - created_at: '2022-04-05T23:50:19.920Z', - updated_at: '2022-04-05T23:50:56.545Z', - }), - ); - - const user = await tabNews.user.me(); - - expect(user).toMatchObject({ - id: 'id', - username: 'username', - email: 'my@email.com', - description: 'description', - notifications: true, - features: [ - 'create:session', - 'read:session', - 'create:content', - 'create:content:text_root', - 'create:content:text_child', - 'update:content', - 'update:user', - ], - tabcoins: 9, - tabcash: 10, - created_at: new Date('2022-04-05T23:50:19.920Z'), - updated_at: new Date('2022-04-05T23:50:56.545Z'), + mockOnceSession(); + + mockOnceResponse(TABNEWS_ENDPOINTS.user, user); + + await tabNews.session.create(); + + const me = await tabNews.user.me(); + + expect(me).toMatchSnapshot(); + + const request = mockedRequest(); + + expectRequest(request).method.toBeGet(); + + expectRequest(request).cookie(TABNEWS_HEADERS.sessionId).toBeDefined(); + }); + + it('should throw an error when has no session', async () => { + mockOnceApiError(TABNEWS_ENDPOINTS.user, { + name: 'ForbiddenError', + message: 'Usuário não pode executar esta operação.', + action: 'Verifique se este usuário possui a feature "read:session".', + status_code: 403, + error_id: '1ca4236b-1a68-4573-acb8-40b220b2fd76', + request_id: 'c17c3107-8cce-4b35-b510-0c1b5b92e3b6', + error_location_code: + 'MODEL:AUTHORIZATION:CAN_REQUEST:FEATURE_NOT_FOUND', }); + + expect(() => tabNews.user.me()).rejects.toThrowErrorMatchingSnapshot(); + + const request = mockedRequest(); + + expectRequest(request).method.toBeGet(); + + expectRequest(request).cookie(TABNEWS_HEADERS.sessionId).toBeUndefined(); }); }); }); diff --git a/src/user/user.ts b/src/user/user.ts index 6d664ba..81dd4e2 100644 --- a/src/user/user.ts +++ b/src/user/user.ts @@ -1,4 +1,6 @@ -import { TabNews } from '../tabnews'; +import { TABNEWS_ENDPOINTS } from '@/commons'; +import { TabNews } from '@/tabnews'; + import { UserResponse } from './interfaces'; export class User { @@ -6,7 +8,7 @@ export class User { async me() { const { body: user } = await this.tabNews.get({ - path: '/user', + path: TABNEWS_ENDPOINTS.user, }); return user; diff --git a/test/setup.ts b/test/setup.ts index 2a690af..3e9b119 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,10 +1,10 @@ -import createFetchMock from 'vitest-fetch-mock'; import { vi } from 'vitest'; +import createFetchMock from 'vitest-fetch-mock'; const fetchMocker = createFetchMock(vi); fetchMocker.enableMocks(); vi.mock('cross-fetch', async (importOriginal) => { - const actual = (await importOriginal()) as unknown as Promise; + const actual = (await importOriginal()) as unknown as typeof fetch; return { ...actual, default: fetchMocker }; }); diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000..2cacf65 --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,135 @@ +import { expect } from 'vitest'; +import { MockParams } from 'vitest-fetch-mock'; + +import { TABNEWS_ENDPOINTS, TABNEWS_HEADERS } from '@/commons'; +import { TabNewsApiError } from '@/commons/interfaces'; +import { TabNewsConfig } from '@/interfaces'; +import { TabNews } from '@/tabnews'; + +export const DEFAULT_TABNEWS_CONFIG: Partial = { + credentials: { + email: 'any@email.com', + password: 'any', + }, +}; +export function createTabNews( + config: Partial = DEFAULT_TABNEWS_CONFIG, +) { + return new TabNews(config); +} + +export function resetMocks() { + fetchMock.resetMocks(); +} + +export function mockOnceResponse( + path: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + body: any, + mockParams?: MockParams, +) { + fetchMock.doMockOnceIf( + (response) => new URL(response.url).pathname.endsWith(path), + JSON.stringify(body), + mockParams, + ); +} + +export function mockOnceApiError(path: string, apiError: TabNewsApiError) { + mockOnceResponse(path, apiError, { + status: apiError.status_code, + }); +} + +export const DEFAULT_SESSION = { + id: '123', + token: 'token123', + expires_at: '2023-10-12T11:56:13.378Z', + created_at: '2023-09-12T11:56:13.379Z', + updated_at: '2023-09-12T11:56:13.379Z', +}; + +export function mockOnceSession( + session: typeof DEFAULT_SESSION = DEFAULT_SESSION, +) { + mockOnceResponse(TABNEWS_ENDPOINTS.session, session); +} + +export function mockedRequests() { + return fetchMock.requests(); +} + +export function mockedRequest() { + const [request] = mockedRequests().reverse(); + return request; +} + +const parseCookie = (cookie: string | null) => + !cookie + ? {} + : cookie + .split(';') + .map((v) => v.split('=')) + .reduce( + (acc, v) => { + acc[decodeURIComponent(v[0].trim())] = decodeURIComponent( + v[1].trim(), + ); + return acc; + }, + {} as Record, + ); + +export function expectRequest(request: Request) { + const url = new URL(request.url); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const toBodyBe = (body: T) => { + expect(request.json()).resolves.toMatchObject(body); + }; + + const toBePost = () => request.method === 'POST'; + const toBeGet = () => request.method === 'GET'; + const toBeDelete = () => request.method === 'DELETE'; + + const header = (headerName: string) => ({ + toBe: (headerValue: string) => + expect(request.headers.get(headerName)).toBe(headerValue), + toBeNull: () => expect(request.headers.get(headerName)).toBeNull(), + }); + + const query = (parameter: string) => ({ + toBe: (parameterValue: string) => + expect(url.searchParams.get(parameter)).toBe(parameterValue), + toBeNull: () => expect(url.searchParams.get(parameter)).toBeNull(), + }); + + const cookie = (cookieName: string) => { + const cookies = parseCookie(request.headers.get(TABNEWS_HEADERS.cookie)); + + return { + toBe: (cookieValue: string) => + expect(cookies[cookieName]).toBe(cookieValue), + toBeDefined: () => expect(cookies[cookieName]).toBeDefined(), + toBeUndefined: () => expect(cookies[cookieName]).toBeUndefined, + }; + }; + + return { + body: { + toBe: toBodyBe, + }, + method: { + toBePost, + toBeGet, + toBeDelete, + }, + header, + query, + cookie, + }; +} + +export async function createSession(tabNews: TabNews) { + return await tabNews.session.create(); +} diff --git a/tsconfig.json b/tsconfig.json index 1ef83a8..0ad18ab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,8 +13,12 @@ "module": "commonjs", "declaration": true, "outDir": "build", - "strict": true + "strict": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@test/*": ["test/*"] + } }, - "include": ["src"], - "exclude": ["**/*.spec.ts"] + "include": ["src", "test"] } diff --git a/vitest.config.ts b/vitest.config.ts index 56152cb..7213d2c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,3 +1,5 @@ +import path from 'path'; + import { defineConfig } from 'vitest/config'; export default defineConfig({ @@ -7,4 +9,10 @@ export default defineConfig({ reporter: ['lcov', 'html'], }, }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + '@test': path.resolve(__dirname, './test'), + }, + }, });