diff --git a/.42c/scan/realworld-conduit-api/scanconf.json b/.42c/scan/realworld-conduit-api/scanconf.json index bdc399a..afd1b38 100644 --- a/.42c/scan/realworld-conduit-api/scanconf.json +++ b/.42c/scan/realworld-conduit-api/scanconf.json @@ -1,1126 +1,1152 @@ { - "version": "2.0.0", - "runtimeConfiguration": { - "environment": "default", - "laxTestingModeEnabled": false, - "apiConnectivityCheck": true, - "logLevel": "error", - "logDestination": "stdout+platform", - "logMaxFileSize": 2147483648, - "requestHeaderNameRequestId": "x-scan-request-id", - "requestHeaderNameScenarioId": "x-scan-scenario-id", - "requestHeaderNameRequestType": "x-scan-request-type", - "requestFlowrate": 100, - "requestTimeout": 30, - "requestTlsInsecureSkipVerify": true, - "responseFollowRedirection": false, - "responseMaxBodySizeScan": 10485760, - "happyPathOnly": false, - "maxRequestRetryAttempts": 5, - "maxScanDuration": 1800, - "memoryLimit": 2147483648, - "memoryTimeSpan": 10, - "reportMaxRequestSizeHappyPath": 8092, - "reportMaxRequestSizeTest": 8092, - "reportIncludeRequestBody": true, - "reportIncludeResponseBody": true, - "reportMaxHttpResponseSizeHappyPath": 8092, - "reportMaxBodySizeHappyPath": 8092, - "reportMaxHttpResponseSizeTest": 8092, - "reportMaxBodySizeTest": 8092, - "reportIssuesOnly": false, - "reportMaxIssues": 1000, - "reportMaxSize": 20971520, - "reportGenerateCurlCommand": true, - "normalizeServerUrls": true + "version": "2.0.0", + "runtimeConfiguration": { + "environment": "default", + "laxTestingModeEnabled": false, + "apiConnectivityCheck": true, + "logLevel": "error", + "logDestination": "stdout+platform", + "logMaxFileSize": 2147483648, + "requestHeaderNameRequestId": "x-scan-request-id", + "requestHeaderNameScenarioId": "x-scan-scenario-id", + "requestHeaderNameRequestType": "x-scan-request-type", + "requestFlowrate": 100, + "requestTimeout": 30, + "requestTlsInsecureSkipVerify": true, + "responseFollowRedirection": false, + "responseMaxBodySizeScan": 10485760, + "happyPathOnly": false, + "maxRequestRetryAttempts": 5, + "maxScanDuration": 1800, + "memoryLimit": 2147483648, + "memoryTimeSpan": 10, + "reportMaxRequestSizeHappyPath": 8092, + "reportMaxRequestSizeTest": 8092, + "reportIncludeRequestBody": true, + "reportIncludeResponseBody": true, + "reportMaxHttpResponseSizeHappyPath": 8092, + "reportMaxBodySizeHappyPath": 8092, + "reportMaxHttpResponseSizeTest": 8092, + "reportMaxBodySizeTest": 8092, + "reportIssuesOnly": false, + "reportMaxIssues": 1000, + "reportMaxSize": 20971520, + "reportGenerateCurlCommand": true, + "normalizeServerUrls": true + }, + "customizations": { + "happyPaths": { + "retry": 1, + "responsePolicy": { + "httpStatusExpected": true, + "mustBeConformant": true + }, + "httpStatusExpected": [] }, - "customizations": { - "happyPaths": { - "retry": 1, - "responsePolicy": { - "httpStatusExpected": true, - "mustBeConformant": true - }, - "httpStatusExpected": [] + "tests": { + "responsePolicy": { + "httpStatusExpected": true, + "mustBeConformant": true + } + } + }, + "environments": { + "default": { + "variables": { + "Token": { + "name": "SCAN42C_SECURITY_TOKEN", + "from": "environment", + "required": true }, - "tests": { - "responsePolicy": { - "httpStatusExpected": true, - "mustBeConformant": true - } - } - }, - "authenticationDetails": [ - { - "Token": { - "type": "apiKey", - "in": "header", - "name": "Authorization", - "default": "Token", - "credentials": { - "Token": { - "description": "Token security", - "credential": "{{Token}}" - } - } - } + "host": { + "name": "SCAN42C_HOST", + "from": "environment", + "required": false, + "default": "https://api.realworld.io/api" } - ], - "operations": { - "CreateArticle": { + } + } + }, + "operations": { + "CreateArticle": { + "operationId": "CreateArticle", + "request": { + "operationId": "CreateArticle", + "auth": [ + "Token" + ], + "request": { + "type": "42c", + "details": { "operationId": "CreateArticle", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/CreateArticle/request", - "fuzzing": true - } - ] - } + "method": "POST", + "url": "{{host}}/articles", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + } ], - "request": { - "operationId": "CreateArticle", - "auth": [ - "Token" - ], - "request": { - "type": "42c", - "details": { - "url": "{{host}}/articles", - "method": "POST", - "headers": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "requestBody": { - "mode": "json", - "json": { - "article": { - "body": "vvgfznbgrmwcrqcxvoxoumzdvihhvbuv", - "description": "xwxrvgrrxbiormmdsukwuajpnoduasau", - "tagList": [ - "gorenkfpfotxwrdtvaaucqycckkktlyu" - ], - "title": "hcdyceqalkgeqadgqebhxhkbnnbqktvh" - } - } - } - } - }, - "defaultResponse": "201", - "responses": { - "201": { - "expectations": { - "httpStatus": 201 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } + "requestBody": { + "mode": "json", + "json": { + "article": { + "body": "vvgfznbgrmwcrqcxvoxoumzdvihhvbuv", + "description": "xwxrvgrrxbiormmdsukwuajpnoduasau", + "tagList": [ + "gorenkfpfotxwrdtvaaucqycckkktlyu" + ], + "title": "hcdyceqalkgeqadgqebhxhkbnnbqktvh" } + } } + } }, - "CreateArticleComment": { + "defaultResponse": "201", + "responses": { + "201": { + "expectations": { + "httpStatus": 201 + } + }, + "401": { + "expectations": { + "httpStatus": 401 + } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/CreateArticle/request" + } + ], + "fuzzing": true + } + ] + }, + "CreateArticleComment": { + "operationId": "CreateArticleComment", + "request": { + "operationId": "CreateArticleComment", + "auth": [ + "Token" + ], + "request": { + "type": "42c", + "details": { "operationId": "CreateArticleComment", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/CreateArticleComment/request", - "fuzzing": true - } - ] - } + "method": "POST", + "url": "{{host}}/articles/{slug}/comments", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "paths": [ + { + "key": "slug", + "value": "aqkjrdchwudvmrcppkdfvyokuqoaskkl" + } ], - "request": { - "operationId": "CreateArticleComment", - "auth": [ - "Token" - ], - "request": { - "type": "42c", - "details": { - "url": "{{host}}/articles/{slug}/comments", - "method": "POST", - "paths": [ - { - "key": "slug", - "value": "aqkjrdchwudvmrcppkdfvyokuqoaskkl" - } - ], - "headers": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "requestBody": { - "mode": "json", - "json": { - "comment": { - "body": "tlciwnbfktptlzhpqpthzfidmevmradu" - } - } - } - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } + "requestBody": { + "mode": "json", + "json": { + "comment": { + "body": "tlciwnbfktptlzhpqpthzfidmevmradu" } + } } + } }, - "CreateArticleFavorite": { - "operationId": "CreateArticleFavorite", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/CreateArticleFavorite/request", - "fuzzing": true - } - ] - } - ], - "request": { - "operationId": "CreateArticleFavorite", - "auth": [ - "Token" - ], - "request": { - "type": "42c", - "details": { - "url": "{{host}}/articles/{slug}/favorite", - "method": "POST", - "paths": [ - { - "key": "slug", - "value": "dtwdghufkvbiytisayhiommkpufwcbxz" - } - ] - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } - } + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "401": { + "expectations": { + "httpStatus": 401 + } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/CreateArticleComment/request" } + ], + "fuzzing": true + } + ] + }, + "CreateArticleFavorite": { + "operationId": "CreateArticleFavorite", + "request": { + "operationId": "CreateArticleFavorite", + "auth": [ + "Token" + ], + "request": { + "type": "42c", + "details": { + "operationId": "CreateArticleFavorite", + "method": "POST", + "url": "{{host}}/articles/{slug}/favorite", + "paths": [ + { + "key": "slug", + "value": "dtwdghufkvbiytisayhiommkpufwcbxz" + } + ] + } }, - "CreateUser": { + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "401": { + "expectations": { + "httpStatus": 401 + } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/CreateArticleFavorite/request" + } + ], + "fuzzing": true + } + ] + }, + "CreateUser": { + "operationId": "CreateUser", + "request": { + "operationId": "CreateUser", + "request": { + "type": "42c", + "details": { "operationId": "CreateUser", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/CreateUser/request", - "fuzzing": true - } - ] - } + "method": "POST", + "url": "{{host}}/users", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + } ], - "request": { - "operationId": "CreateUser", - "request": { - "type": "42c", - "details": { - "url": "{{host}}/users", - "method": "POST", - "headers": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "requestBody": { - "mode": "json", - "json": { - "user": { - "email": "keteszncchuuohfhpchqrkeenlgbcqgi", - "password": "fkzalegmpqpkshntrjtnvvyjwpwoqfvk", - "username": "ifxatyshpfogjsdhsnwqkxswsqwrfbca" - } - } - } - } - }, - "defaultResponse": "201", - "responses": { - "201": { - "expectations": { - "httpStatus": 201 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } + "requestBody": { + "mode": "json", + "json": { + "user": { + "email": "keteszncchuuohfhpchqrkeenlgbcqgi", + "password": "fkzalegmpqpkshntrjtnvvyjwpwoqfvk", + "username": "ifxatyshpfogjsdhsnwqkxswsqwrfbca" } + } } + } }, - "DeleteArticle": { - "operationId": "DeleteArticle", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/DeleteArticle/request", - "fuzzing": true - } - ] - } - ], - "request": { - "operationId": "DeleteArticle", - "auth": [ - "Token" - ], - "request": { - "type": "42c", - "details": { - "url": "{{host}}/articles/{slug}", - "method": "DELETE", - "paths": [ - { - "key": "slug", - "value": "lzxbnaadnpaghirykndzppnwzqgtesdx" - } - ] - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } - } + "defaultResponse": "201", + "responses": { + "201": { + "expectations": { + "httpStatus": 201 + }, + "variableAssignments": { + "varname": { + "from": "response", + "in": "header", + "name": "name" + } + } + }, + "422": { + "expectations": { + "httpStatus": 422 } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/CreateUser/request" + } + ], + "fuzzing": true + } + ] + }, + "DeleteArticle": { + "operationId": "DeleteArticle", + "request": { + "operationId": "DeleteArticle", + "auth": [ + "Token" + ], + "request": { + "type": "42c", + "details": { + "operationId": "DeleteArticle", + "method": "DELETE", + "url": "{{host}}/articles/{slug}", + "paths": [ + { + "key": "slug", + "value": "lzxbnaadnpaghirykndzppnwzqgtesdx" + } + ] + } }, - "DeleteArticleComment": { - "operationId": "DeleteArticleComment", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/DeleteArticleComment/request", - "fuzzing": true - } - ] - } - ], - "request": { - "operationId": "DeleteArticleComment", - "auth": [ - "Token" - ], - "request": { - "type": "42c", - "details": { - "url": "{{host}}/articles/{slug}/comments/{id}", - "method": "DELETE", - "paths": [ - { - "key": "id", - "value": 8232536480128643033 - }, - { - "key": "slug", - "value": "yudwgwanlubcueakrfzttzxkbylxtohy" - } - ] - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } - } + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "401": { + "expectations": { + "httpStatus": 401 + } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/DeleteArticle/request" } + ], + "fuzzing": true + } + ] + }, + "DeleteArticleComment": { + "operationId": "DeleteArticleComment", + "request": { + "operationId": "DeleteArticleComment", + "auth": [ + "Token" + ], + "request": { + "type": "42c", + "details": { + "operationId": "DeleteArticleComment", + "method": "DELETE", + "url": "{{host}}/articles/{slug}/comments/{id}", + "paths": [ + { + "key": "id", + "value": 8232536480128643000 + }, + { + "key": "slug", + "value": "yudwgwanlubcueakrfzttzxkbylxtohy" + } + ] + } }, - "DeleteArticleFavorite": { - "operationId": "DeleteArticleFavorite", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/DeleteArticleFavorite/request", - "fuzzing": true - } - ] - } - ], - "request": { - "operationId": "DeleteArticleFavorite", - "auth": [ - "Token" - ], - "request": { - "type": "42c", - "details": { - "url": "{{host}}/articles/{slug}/favorite", - "method": "DELETE", - "paths": [ - { - "key": "slug", - "value": "wybjmacxnyktvsklxfnecdqnkgadxpxd" - } - ] - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } - } + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "401": { + "expectations": { + "httpStatus": 401 } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/DeleteArticleComment/request" + } + ], + "fuzzing": true + } + ] + }, + "DeleteArticleFavorite": { + "operationId": "DeleteArticleFavorite", + "request": { + "operationId": "DeleteArticleFavorite", + "auth": [ + "Token" + ], + "request": { + "type": "42c", + "details": { + "operationId": "DeleteArticleFavorite", + "method": "DELETE", + "url": "{{host}}/articles/{slug}/favorite", + "paths": [ + { + "key": "slug", + "value": "wybjmacxnyktvsklxfnecdqnkgadxpxd" + } + ] + } }, - "FollowUserByUsername": { - "operationId": "FollowUserByUsername", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/FollowUserByUsername/request", - "fuzzing": true - } - ] - } - ], - "request": { - "operationId": "FollowUserByUsername", - "auth": [ - "Token" - ], - "request": { - "type": "42c", - "details": { - "url": "{{host}}/profiles/{username}/follow", - "method": "POST", - "paths": [ - { - "key": "username", - "value": "xpthffmsepkdvqaxjeoxolnngyxeywwo" - } - ] - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } - } + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "401": { + "expectations": { + "httpStatus": 401 } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/DeleteArticleFavorite/request" + } + ], + "fuzzing": true + } + ] + }, + "FollowUserByUsername": { + "operationId": "FollowUserByUsername", + "request": { + "operationId": "FollowUserByUsername", + "auth": [ + "Token" + ], + "request": { + "type": "42c", + "details": { + "operationId": "FollowUserByUsername", + "method": "POST", + "url": "{{host}}/profiles/{username}/follow", + "paths": [ + { + "key": "username", + "value": "xpthffmsepkdvqaxjeoxolnngyxeywwo" + } + ] + } }, - "GetArticle": { - "operationId": "GetArticle", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/GetArticle/request", - "fuzzing": true - } - ] - } - ], - "request": { - "operationId": "GetArticle", - "request": { - "type": "42c", - "details": { - "url": "{{host}}/articles/{slug}", - "method": "GET", - "paths": [ - { - "key": "slug", - "value": "uqvyesxpenboyjaieosrzaijkraklxrv" - } - ] - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } - } + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 } + }, + "401": { + "expectations": { + "httpStatus": 401 + } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/FollowUserByUsername/request" + } + ], + "fuzzing": true + } + ] + }, + "GetArticle": { + "operationId": "GetArticle", + "request": { + "operationId": "GetArticle", + "request": { + "type": "42c", + "details": { + "operationId": "GetArticle", + "method": "GET", + "url": "{{host}}/articles/{slug}", + "paths": [ + { + "key": "slug", + "value": "uqvyesxpenboyjaieosrzaijkraklxrv" + } + ] + } }, - "GetArticleComments": { - "operationId": "GetArticleComments", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/GetArticleComments/request", - "fuzzing": true - } - ] - } - ], - "request": { - "operationId": "GetArticleComments", - "request": { - "type": "42c", - "details": { - "url": "{{host}}/articles/{slug}/comments", - "method": "GET", - "paths": [ - { - "key": "slug", - "value": "vzczgtkzqfehkzlgwykjykngnegqoizq" - } - ] - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } - } + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/GetArticle/request" } + ], + "fuzzing": true + } + ] + }, + "GetArticleComments": { + "operationId": "GetArticleComments", + "request": { + "operationId": "GetArticleComments", + "request": { + "type": "42c", + "details": { + "operationId": "GetArticleComments", + "method": "GET", + "url": "{{host}}/articles/{slug}/comments", + "paths": [ + { + "key": "slug", + "value": "vzczgtkzqfehkzlgwykjykngnegqoizq" + } + ] + } }, - "GetArticles": { - "operationId": "GetArticles", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/GetArticles/request", - "fuzzing": true - } - ] - } - ], - "request": { - "operationId": "GetArticles", - "request": { - "type": "42c", - "details": { - "url": "{{host}}/articles", - "method": "GET", - "queries": [ - { - "key": "tag", - "value": "nycjkayzbnvceyastidrdfuqonhefamq" - }, - { - "key": "author", - "value": "iptvtwysbshqzxjqzeqoivtnvxafocgr" - }, - { - "key": "favorited", - "value": "yemwendxbevpoiuvgesnhkdqrnuwtyek" - }, - { - "key": "offset", - "value": 5422773520838902743 - }, - { - "key": "limit", - "value": 20 - } - ] - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } - } + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 } + }, + "401": { + "expectations": { + "httpStatus": 401 + } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/GetArticleComments/request" + } + ], + "fuzzing": true + } + ] + }, + "GetArticles": { + "operationId": "GetArticles", + "request": { + "operationId": "GetArticles", + "request": { + "type": "42c", + "details": { + "operationId": "GetArticles", + "method": "GET", + "url": "{{host}}/articles", + "queries": [ + { + "key": "tag", + "value": "nycjkayzbnvceyastidrdfuqonhefamq" + }, + { + "key": "author", + "value": "iptvtwysbshqzxjqzeqoivtnvxafocgr" + }, + { + "key": "favorited", + "value": "yemwendxbevpoiuvgesnhkdqrnuwtyek" + }, + { + "key": "offset", + "value": 5422773520838903000 + }, + { + "key": "limit", + "value": 20 + } + ] + } }, - "GetArticlesFeed": { - "operationId": "GetArticlesFeed", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/GetArticlesFeed/request", - "fuzzing": true - } - ] - } - ], - "request": { - "operationId": "GetArticlesFeed", - "auth": [ - "Token" - ], - "request": { - "type": "42c", - "details": { - "url": "{{host}}/articles/feed", - "method": "GET", - "queries": [ - { - "key": "offset", - "value": 1058701487655261858 - }, - { - "key": "limit", - "value": 20 - } - ] - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } - } + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "401": { + "expectations": { + "httpStatus": 401 + } + }, + "422": { + "expectations": { + "httpStatus": 422 } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/GetArticles/request" + } + ], + "fuzzing": true + } + ] + }, + "GetArticlesFeed": { + "operationId": "GetArticlesFeed", + "request": { + "operationId": "GetArticlesFeed", + "auth": [ + "Token" + ], + "request": { + "type": "42c", + "details": { + "operationId": "GetArticlesFeed", + "method": "GET", + "url": "{{host}}/articles/feed", + "queries": [ + { + "key": "offset", + "value": 1058701487655261800 + }, + { + "key": "limit", + "value": 20 + } + ] + } }, - "GetCurrentUser": { - "operationId": "GetCurrentUser", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/GetCurrentUser/request", - "fuzzing": true - } - ] - } - ], - "request": { - "operationId": "GetCurrentUser", - "auth": [ - "Token" - ], - "request": { - "type": "42c", - "details": { - "url": "{{host}}/user", - "method": "GET" - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } - } + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "401": { + "expectations": { + "httpStatus": 401 + } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/GetArticlesFeed/request" } + ], + "fuzzing": true + } + ] + }, + "GetCurrentUser": { + "operationId": "GetCurrentUser", + "request": { + "operationId": "GetCurrentUser", + "auth": [ + "Token" + ], + "request": { + "type": "42c", + "details": { + "operationId": "GetCurrentUser", + "method": "GET", + "url": "{{host}}/user" + } }, - "GetProfileByUsername": { - "operationId": "GetProfileByUsername", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/GetProfileByUsername/request", - "fuzzing": true - } - ] - } - ], - "request": { - "operationId": "GetProfileByUsername", - "request": { - "type": "42c", - "details": { - "url": "{{host}}/profiles/{username}", - "method": "GET", - "paths": [ - { - "key": "username", - "value": "fjzuejgyjhjfqytzidfizgflojqzsuxz" - } - ] - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } - } + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "401": { + "expectations": { + "httpStatus": 401 } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/GetCurrentUser/request" + } + ], + "fuzzing": true + } + ] + }, + "GetProfileByUsername": { + "operationId": "GetProfileByUsername", + "request": { + "operationId": "GetProfileByUsername", + "request": { + "type": "42c", + "details": { + "operationId": "GetProfileByUsername", + "method": "GET", + "url": "{{host}}/profiles/{username}", + "paths": [ + { + "key": "username", + "value": "fjzuejgyjhjfqytzidfizgflojqzsuxz" + } + ] + } }, - "GetTags": { - "operationId": "GetTags", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/GetTags/request", - "fuzzing": true - } - ] - } - ], - "request": { - "operationId": "GetTags", - "request": { - "type": "42c", - "details": { - "url": "{{host}}/tags", - "method": "GET" - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } - } + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "401": { + "expectations": { + "httpStatus": 401 } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/GetProfileByUsername/request" + } + ], + "fuzzing": true + } + ] + }, + "GetTags": { + "operationId": "GetTags", + "request": { + "operationId": "GetTags", + "request": { + "type": "42c", + "details": { + "operationId": "GetTags", + "method": "GET", + "url": "{{host}}/tags" + } }, - "Login": { + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/GetTags/request" + } + ], + "fuzzing": true + } + ] + }, + "Login": { + "operationId": "Login", + "request": { + "operationId": "Login", + "request": { + "type": "42c", + "details": { "operationId": "Login", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/Login/request", - "fuzzing": true - } - ] - } + "method": "POST", + "url": "{{host}}/users/login", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + } ], - "request": { - "operationId": "Login", - "request": { - "type": "42c", - "details": { - "url": "{{host}}/users/login", - "method": "POST", - "headers": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "requestBody": { - "mode": "json", - "json": { - "user": { - "email": "vnqpuvhiofiqksewdebfvlgbvyidlabt", - "password": "culpctdirisgeqbbzwvgqitpeyoatqde" - } - } - } - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } + "requestBody": { + "mode": "json", + "json": { + "user": { + "email": "vnqpuvhiofiqksewdebfvlgbvyidlabt", + "password": "culpctdirisgeqbbzwvgqitpeyoatqde" } + } } + } }, - "UnfollowUserByUsername": { - "operationId": "UnfollowUserByUsername", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/UnfollowUserByUsername/request", - "fuzzing": true - } - ] - } - ], - "request": { - "operationId": "UnfollowUserByUsername", - "auth": [ - "Token" - ], - "request": { - "type": "42c", - "details": { - "url": "{{host}}/profiles/{username}/follow", - "method": "DELETE", - "paths": [ - { - "key": "username", - "value": "lojwihcptgcmhwlqeisqkmktzyzehypr" - } - ] - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } - } + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "401": { + "expectations": { + "httpStatus": 401 + } + }, + "422": { + "expectations": { + "httpStatus": 422 } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/Login/request" + } + ], + "fuzzing": true + } + ] + }, + "UnfollowUserByUsername": { + "operationId": "UnfollowUserByUsername", + "request": { + "operationId": "UnfollowUserByUsername", + "auth": [ + "Token" + ], + "request": { + "type": "42c", + "details": { + "operationId": "UnfollowUserByUsername", + "method": "DELETE", + "url": "{{host}}/profiles/{username}/follow", + "paths": [ + { + "key": "username", + "value": "lojwihcptgcmhwlqeisqkmktzyzehypr" + } + ] + } }, - "UpdateArticle": { + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "401": { + "expectations": { + "httpStatus": 401 + } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/UnfollowUserByUsername/request" + } + ], + "fuzzing": true + } + ] + }, + "UpdateArticle": { + "operationId": "UpdateArticle", + "request": { + "operationId": "UpdateArticle", + "auth": [ + "Token" + ], + "request": { + "type": "42c", + "details": { "operationId": "UpdateArticle", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/UpdateArticle/request", - "fuzzing": true - } - ] - } + "method": "PUT", + "url": "{{host}}/articles/{slug}", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "paths": [ + { + "key": "slug", + "value": "mjluztmujyzhhqloaxwwgsxvdksrfmzj" + } ], - "request": { - "operationId": "UpdateArticle", - "auth": [ - "Token" - ], - "request": { - "type": "42c", - "details": { - "url": "{{host}}/articles/{slug}", - "method": "PUT", - "paths": [ - { - "key": "slug", - "value": "mjluztmujyzhhqloaxwwgsxvdksrfmzj" - } - ], - "headers": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "requestBody": { - "mode": "json", - "json": { - "article": { - "body": "zekmmewqubejserxahyjmfpsymlkzwqs", - "description": "ahivgphzlpezbfbfqjsjsktoiqnxwjgu", - "title": "zpvzptjgzhdxhxiwnckuzodbekxrtdlv" - } - } - } - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } + "requestBody": { + "mode": "json", + "json": { + "article": { + "body": "zekmmewqubejserxahyjmfpsymlkzwqs", + "description": "ahivgphzlpezbfbfqjsjsktoiqnxwjgu", + "title": "zpvzptjgzhdxhxiwnckuzodbekxrtdlv" } + } } + } }, - "UpdateCurrentUser": { + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "401": { + "expectations": { + "httpStatus": 401 + } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } + } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/UpdateArticle/request" + } + ], + "fuzzing": true + } + ] + }, + "UpdateCurrentUser": { + "operationId": "UpdateCurrentUser", + "request": { + "operationId": "UpdateCurrentUser", + "auth": [ + "Token" + ], + "request": { + "type": "42c", + "details": { "operationId": "UpdateCurrentUser", - "scenarios": [ - { - "key": "happy.path", - "fuzzing": true, - "requests": [ - { - "$ref": "#/operations/UpdateCurrentUser/request", - "fuzzing": true - } - ] - } + "method": "PUT", + "url": "{{host}}/user", + "headers": [ + { + "key": "Content-Type", + "value": "application/json" + } ], - "request": { - "operationId": "UpdateCurrentUser", - "auth": [ - "Token" - ], - "request": { - "type": "42c", - "details": { - "url": "{{host}}/user", - "method": "PUT", - "headers": [ - { - "key": "Content-Type", - "value": "application/json" - } - ], - "requestBody": { - "mode": "json", - "json": { - "user": { - "bio": "knvupibzksitsfgjfealuunurcahgsfe", - "email": "aevypatdjkvmoyyhecwzqxzygrffhjov", - "image": "qlidptrxtwanvwcpihvfnillqdbscxcz", - "password": "eaphqvckvozvwuqtwirupdyskqcamemh", - "username": "emtijslhduugxqtadtrnezpqfuufogby" - } - } - } - } - }, - "defaultResponse": "200", - "responses": { - "200": { - "expectations": { - "httpStatus": 200 - } - }, - "401": { - "expectations": { - "httpStatus": 401 - } - }, - "422": { - "expectations": { - "httpStatus": 422 - } - } + "requestBody": { + "mode": "json", + "json": { + "user": { + "bio": "knvupibzksitsfgjfealuunurcahgsfe", + "email": "aevypatdjkvmoyyhecwzqxzygrffhjov", + "image": "qlidptrxtwanvwcpihvfnillqdbscxcz", + "password": "eaphqvckvozvwuqtwirupdyskqcamemh", + "username": "emtijslhduugxqtadtrnezpqfuufogby" } + } + } + } + }, + "defaultResponse": "200", + "responses": { + "200": { + "expectations": { + "httpStatus": 200 + } + }, + "401": { + "expectations": { + "httpStatus": 401 } + }, + "422": { + "expectations": { + "httpStatus": 422 + } + } } - }, - "environments": { - "default": { - "variables": { - "Token": { - "from": "environment", - "name": "SCAN42C_SECURITY_TOKEN", - "required": true - }, - "host": { - "from": "environment", - "name": "SCAN42C_HOST", - "required": false, - "default": "https://api.realworld.io/api" - } + }, + "scenarios": [ + { + "key": "happy.path", + "requests": [ + { + "fuzzing": true, + "$ref": "#/operations/UpdateCurrentUser/request" } + ], + "fuzzing": true + } + ] + } + }, + "authenticationDetails": [ + { + "Token": { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "default": "Token", + "credentials": { + "Token": { + "credential": "{{Token}}", + "description": "Token security" + } } + } } + ] } \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..10c9de2 --- /dev/null +++ b/.env.development @@ -0,0 +1 @@ +VITE_API_BASE_URL='https://api.realworld.show/api' \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3bbdfb5..b83d42e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -48,8 +48,7 @@ jobs: # 第六步:类型检查(TypeScript) - name: Type check - run: pnpm type-check - + run: pnpm exec vue-tsc -p tsconfig.app.json --noEmit # 第七步:运行单元测试和组件测试(Vitest) - name: Unit & component tests run: pnpm test:run diff --git a/README.md b/README.md index 15c0d32..dd7477c 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,10 @@ This project will include basic features such as user authentication, user profi * I created basic stores and set up Vue Router today. * I also added a simple test and got a feel for the CI workflow. * With the help of AI, I drafted a deployment workflow. -* I don't fully understand the deployment file yet, but I'm digging into them and the YAML syntax. \ No newline at end of file +* I don't fully understand the deployment file yet, but I'm digging into them and the YAML syntax. + +### DAY3 +* I wrote and tested an HTTP request mock. +* I explored some configuration files to get familiar with the basic setup. +* I haven't started writing actual components or logic yet. I'm focusing on getting familiar with the basic structure of a frontend project. +* My CI workflow failed. I created a new account to be my reviewer. However, when I approved the pull request through my new account, the dev branch wasn't merged into the main branch. I'm not sure why this happened, so I decided to disable the rule set temporarily. \ No newline at end of file diff --git a/components.d.ts b/components.d.ts new file mode 100644 index 0000000..74c38b1 --- /dev/null +++ b/components.d.ts @@ -0,0 +1,36 @@ +/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +// biome-ignore lint: disable +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + Article: typeof import('./src/components/Article.vue')['default'] + ElButton: typeof import('element-plus/es')['ElButton'] + ElCard: typeof import('element-plus/es')['ElCard'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElMenu: typeof import('element-plus/es')['ElMenu'] + ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] + ElTag: typeof import('element-plus/es')['ElTag'] + Footer: typeof import('./src/components/Footer.vue')['default'] + GlobalFeed: typeof import('./src/components/GlobalFeed.vue')['default'] + HelloWorld: typeof import('./src/components/HelloWorld.vue')['default'] + Home: typeof import('./src/components/Home.vue')['default'] + Layout: typeof import('./src/components/Layout.vue')['default'] + Login: typeof import('./src/components/Login.vue')['default'] + Me: typeof import('./src/components/Me.vue')['default'] + Navbar: typeof import('./src/components/Navbar.vue')['default'] + NewArticle: typeof import('./src/components/NewArticle.vue')['default'] + Register: typeof import('./src/components/Register.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + Setting: typeof import('./src/components/Setting.vue')['default'] + YourFeed: typeof import('./src/components/YourFeed.vue')['default'] + } +} diff --git a/mock/browser.ts b/mock/browser.ts new file mode 100644 index 0000000..e69de29 diff --git a/mock/handlers.ts b/mock/handlers.ts new file mode 100644 index 0000000..0eefa2c --- /dev/null +++ b/mock/handlers.ts @@ -0,0 +1,18 @@ +import { http, HttpResponse } from 'msw' +interface User { + email: string; + password: string; + username: string; +} +// simulate the register workflow. +export const handlers = [ + http.get('http://localhost:5173/user', async () => { + return HttpResponse.json({ + email: 'xxx@gmail.com', + token: 'TEST', + username: 'xxx', + bio: 'bio desc', + image: 'img' + }) + }) +] \ No newline at end of file diff --git a/mock/node.ts b/mock/node.ts new file mode 100644 index 0000000..abbedd7 --- /dev/null +++ b/mock/node.ts @@ -0,0 +1,4 @@ +import { setupServer } from 'msw/node' +import { handlers } from './handlers' + +export const server = setupServer(...handlers) \ No newline at end of file diff --git a/package.json b/package.json index 0f2869f..843ea0e 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,13 @@ ] }, "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "@types/path-browserify": "^1.0.3", "axios": "^1.11.0", "element-plus": "^2.11.1", + "path-browserify": "^1.0.1", "pinia": "^3.0.3", + "url": "^0.11.4", "vue": "^3.5.18", "vue-router": "^4.5.1" }, @@ -46,10 +50,13 @@ "husky": "^9.1.7", "jsdom": "^26.1.0", "lint-staged": "^16.1.6", + "msw": "^2.11.1", "prettier": "^3.6.2", "prettier-plugin-tailwindcss": "^0.6.14", "tailwindcss": "^4.1.12", "typescript": "^5.8.3", + "unplugin-auto-import": "^20.1.0", + "unplugin-vue-components": "^29.0.0", "vite": "^7.1.2", "vitest": "^3.2.4", "vue-tsc": "^3.0.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09954ab..e526aff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,15 +8,27 @@ importers: .: dependencies: + '@element-plus/icons-vue': + specifier: ^2.3.2 + version: 2.3.2(vue@3.5.21(typescript@5.8.3)) + '@types/path-browserify': + specifier: ^1.0.3 + version: 1.0.3 axios: specifier: ^1.11.0 version: 1.11.0 element-plus: specifier: ^2.11.1 version: 2.11.1(vue@3.5.21(typescript@5.8.3)) + path-browserify: + specifier: ^1.0.1 + version: 1.0.1 pinia: specifier: ^3.0.3 version: 3.0.3(typescript@5.8.3)(vue@3.5.21(typescript@5.8.3)) + url: + specifier: ^0.11.4 + version: 0.11.4 vue: specifier: ^3.5.18 version: 3.5.21(typescript@5.8.3) @@ -44,7 +56,7 @@ importers: version: 6.0.1(vite@7.1.4(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1))(vue@3.5.21(typescript@5.8.3)) '@vitest/coverage-v8': specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(yaml@2.8.1)) + version: 3.2.4(vitest@3.2.4(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.11.1(typescript@5.8.3))(yaml@2.8.1)) '@vue/test-utils': specifier: ^2.4.6 version: 2.4.6 @@ -69,6 +81,9 @@ importers: lint-staged: specifier: ^16.1.6 version: 16.1.6 + msw: + specifier: ^2.11.1 + version: 2.11.1(typescript@5.8.3) prettier: specifier: ^3.6.2 version: 3.6.2 @@ -81,12 +96,18 @@ importers: typescript: specifier: ^5.8.3 version: 5.8.3 + unplugin-auto-import: + specifier: ^20.1.0 + version: 20.1.0(@vueuse/core@9.13.0(vue@3.5.21(typescript@5.8.3))) + unplugin-vue-components: + specifier: ^29.0.0 + version: 29.0.0(@babel/parser@7.28.3)(vue@3.5.21(typescript@5.8.3)) vite: specifier: ^7.1.2 version: 7.1.4(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1) vitest: specifier: ^3.2.4 - version: 3.2.4(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(yaml@2.8.1) + version: 3.2.4(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.11.1(typescript@5.8.3))(yaml@2.8.1) vue-tsc: specifier: ^3.0.5 version: 3.0.6(typescript@5.8.3) @@ -129,6 +150,12 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@bundled-es-modules/cookie@2.0.1': + resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} + + '@bundled-es-modules/statuses@1.0.1': + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -389,6 +416,37 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@inquirer/confirm@5.1.16': + resolution: {integrity: sha512-j1a5VstaK5KQy8Mu8cHmuQvN1Zc62TbLhjJxwHvKPPKEoowSF6h/0UdOpA9DNdWZ+9Inq73+puRq1df6OJ8Sag==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.2.0': + resolution: {integrity: sha512-NyDSjPqhSvpZEMZrLCYUquWNl+XC/moEcVFqS55IEYIYsY0a1cUCevSqk7ctOlnm/RaSBU5psFryNlxcmGrjaA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.13': + resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.8': + resolution: {integrity: sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -417,6 +475,10 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@mswjs/interceptors@0.39.6': + resolution: {integrity: sha512-bndDP83naYYkfayr/qhBHMhk0YGwS1iv6vaEGcr0SQbO0IZtbOPqjKjds/WcG+bJA+1T5vCx6kprKOzn5Bg+Vw==} + engines: {node: '>=18'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -432,6 +494,15 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -657,6 +728,9 @@ packages: '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -672,6 +746,12 @@ packages: '@types/lodash@4.17.20': resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + '@types/path-browserify@1.0.3': + resolution: {integrity: sha512-ZmHivEbNCBtAfcrFeBCiTjdIc2dey0l7oCGNGpSuRTy8jP6UVND7oUowlvDujBy8r2Hoa8bfFUOCiPWfmtkfxw==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/web-bluetooth@0.0.16': resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} @@ -887,6 +967,10 @@ packages: alien-signals@2.0.7: resolution: {integrity: sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg==} + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-escapes@7.0.0: resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} engines: {node: '>=18'} @@ -911,6 +995,10 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -944,6 +1032,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + birpc@2.5.0: resolution: {integrity: sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==} @@ -996,6 +1088,10 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -1008,6 +1104,14 @@ packages: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1033,9 +1137,19 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + copy-anything@3.0.5: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} engines: {node: '>=12.13'} @@ -1177,6 +1291,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -1184,6 +1302,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-config-prettier@10.1.8: resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true @@ -1256,6 +1378,9 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1332,6 +1457,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.3.1: resolution: {integrity: sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==} engines: {node: '>=18'} @@ -1370,6 +1499,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql@16.11.0: + resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -1397,6 +1530,9 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -1459,6 +1595,10 @@ packages: resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} engines: {node: '>= 0.4'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-boolean-object@1.2.2: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} @@ -1495,6 +1635,9 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -1688,6 +1831,10 @@ packages: resolution: {integrity: sha512-0aeh5HHHgmq1KRdMMDHfhMWQmIT/m7nRDTlxlFqni2Sp0had9baqsjJRvDGdlvgd6NmPE0nPloOipiQJGFtTHQ==} engines: {node: '>=20.0.0'} + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1786,12 +1933,29 @@ packages: engines: {node: '>=10'} hasBin: true + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.11.1: + resolution: {integrity: sha512-dGSRx0AJmQVQfpGXTsAAq4JFdwdhOBdJ6sJS/jnN0ac3s0NZB6daacHF1z5Pefx+IejmvuiLWw260RlyQOf3sQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + nano-spawn@1.0.2: resolution: {integrity: sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==} engines: {node: '>=20.17'} @@ -1809,6 +1973,10 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} hasBin: true + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + normalize-wheel-es@1.2.0: resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} @@ -1842,6 +2010,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1875,6 +2046,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1910,6 +2084,12 @@ packages: typescript: optional: true + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -2002,20 +2182,38 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2053,6 +2251,9 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -2116,6 +2317,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -2123,6 +2328,9 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -2205,10 +2413,17 @@ packages: tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tldts-core@7.0.12: + resolution: {integrity: sha512-3K76aXywJFduGRsOYoY5JzINLs/WMlOkeDwPL+8OCPq2Rh39gkSDtWAxdJQlWjpun/xF/LHf29yqCi6VC/rHDA==} + tldts@6.1.86: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + tldts@7.0.12: + resolution: {integrity: sha512-M9ZQBPp6FyqhMcl233vHYyYRkxXOA1SKGlnq13S0mJdUhRSwr2w6I8rlchPL73wBwRlyIZpFvpu2VcdSMWLYXw==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2217,6 +2432,10 @@ packages: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + tr46@5.1.1: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} @@ -2231,14 +2450,70 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + typescript@5.8.3: resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} hasBin: true + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + unimport@5.2.0: + resolution: {integrity: sha512-bTuAMMOOqIAyjV4i4UH7P07pO+EsVxmhOzQ2YJ290J6mkLUdozNhb5I/YoOEheeNADC03ent3Qj07X0fWfUpmw==} + engines: {node: '>=18.12.0'} + + unplugin-auto-import@20.1.0: + resolution: {integrity: sha512-Wa7/y3DwpbxhjyXCbuliuATCPa0/e47tstWkytJGAr55ooSNwIvbkrq0rlduqYGiCNMsZcD+C6vsN+W3AX96eA==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': ^4.0.0 + '@vueuse/core': '*' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@vueuse/core': + optional: true + + unplugin-utils@0.2.5: + resolution: {integrity: sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==} + engines: {node: '>=18.12.0'} + + unplugin-utils@0.3.0: + resolution: {integrity: sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==} + engines: {node: '>=20.19.0'} + + unplugin-vue-components@29.0.0: + resolution: {integrity: sha512-M2DX44g4/jvBkB0V6uwqTbkTd5DMRHpeGoi/cIKwGG4HPuNxLbe8zoTStB2n12hoDiWc9I1PIRQruRWExNXHlQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/parser': ^7.15.8 + '@nuxt/kit': ^3.2.2 || ^4.0.0 + vue: 2 || 3 + peerDependenciesMeta: + '@babel/parser': + optional: true + '@nuxt/kit': + optional: true + + unplugin@2.3.10: + resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} + engines: {node: '>=18.12.0'} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url@0.11.4: + resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} + engines: {node: '>= 0.4'} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2365,6 +2640,9 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -2403,6 +2681,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2438,6 +2720,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -2447,10 +2733,22 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + snapshots: '@ampproject/remapping@2.3.0': @@ -2489,6 +2787,14 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@bundled-es-modules/cookie@2.0.1': + dependencies: + cookie: 0.7.2 + + '@bundled-es-modules/statuses@1.0.1': + dependencies: + statuses: 2.0.2 + '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -2661,6 +2967,26 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@inquirer/confirm@5.1.16': + dependencies: + '@inquirer/core': 10.2.0 + '@inquirer/type': 3.0.8 + + '@inquirer/core@10.2.0': + dependencies: + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8 + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/figures@1.0.13': {} + + '@inquirer/type@3.0.8': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2695,6 +3021,15 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mswjs/interceptors@0.39.6': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2709,6 +3044,15 @@ snapshots: '@one-ini/wasm@0.1.1': {} + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -2876,6 +3220,8 @@ snapshots: dependencies: '@types/deep-eql': 4.0.2 + '@types/cookie@0.6.0': {} + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} @@ -2888,6 +3234,10 @@ snapshots: '@types/lodash@4.17.20': {} + '@types/path-browserify@1.0.3': {} + + '@types/statuses@2.0.6': {} + '@types/web-bluetooth@0.0.16': {} '@typescript-eslint/eslint-plugin@8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.8.3))(eslint@9.34.0(jiti@2.5.1))(typescript@5.8.3)': @@ -2989,7 +3339,7 @@ snapshots: vite: 7.1.4(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1) vue: 3.5.21(typescript@5.8.3) - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(yaml@2.8.1))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.11.1(typescript@5.8.3))(yaml@2.8.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -3004,7 +3354,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(yaml@2.8.1) + vitest: 3.2.4(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.11.1(typescript@5.8.3))(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -3016,12 +3366,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.4(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(msw@2.11.1(typescript@5.8.3))(vite@7.1.4(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.18 optionalDependencies: + msw: 2.11.1(typescript@5.8.3) vite: 7.1.4(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': @@ -3202,6 +3553,10 @@ snapshots: alien-signals@2.0.7: {} + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-escapes@7.0.0: dependencies: environment: 1.1.0 @@ -3218,6 +3573,11 @@ snapshots: ansi-styles@6.2.1: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + argparse@2.0.1: {} aria-query@5.1.3: @@ -3255,6 +3615,8 @@ snapshots: balanced-match@1.0.2: {} + binary-extensions@2.3.0: {} + birpc@2.5.0: {} boolbase@1.0.0: {} @@ -3310,6 +3672,18 @@ snapshots: check-error@2.1.1: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chownr@3.0.0: {} cli-cursor@5.0.0: @@ -3321,6 +3695,14 @@ snapshots: slice-ansi: 5.0.0 string-width: 7.2.0 + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3339,11 +3721,17 @@ snapshots: concat-map@0.0.1: {} + confbox@0.1.8: {} + + confbox@0.2.2: {} + config-chain@1.1.13: dependencies: ini: 1.3.8 proto-list: 1.2.4 + cookie@0.7.2: {} + copy-anything@3.0.5: dependencies: is-what: 4.1.16 @@ -3532,10 +3920,14 @@ snapshots: '@esbuild/win32-ia32': 0.25.9 '@esbuild/win32-x64': 0.25.9 + escalade@3.2.0: {} + escape-html@1.0.3: {} escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-config-prettier@10.1.8(eslint@9.34.0(jiti@2.5.1)): dependencies: eslint: 9.34.0(jiti@2.5.1) @@ -3632,6 +4024,8 @@ snapshots: expect-type@1.2.2: {} + exsolve@1.0.7: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -3700,6 +4094,8 @@ snapshots: functions-have-names@1.2.3: {} + get-caller-file@2.0.5: {} + get-east-asian-width@1.3.1: {} get-intrinsic@1.3.0: @@ -3745,6 +4141,8 @@ snapshots: graphemer@1.4.0: {} + graphql@16.11.0: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -3765,6 +4163,8 @@ snapshots: he@1.2.0: {} + headers-polyfill@4.0.3: {} + hookable@5.5.3: {} html-encoding-sniffer@4.0.0: @@ -3827,6 +4227,10 @@ snapshots: dependencies: has-bigints: 1.1.0 + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + is-boolean-object@1.2.2: dependencies: call-bound: 1.0.4 @@ -3855,6 +4259,8 @@ snapshots: is-map@2.0.3: {} + is-node-process@1.2.0: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -4061,6 +4467,12 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.0 + local-pkg@1.1.2: + dependencies: + mlly: 1.8.0 + pkg-types: 2.3.0 + quansync: 0.2.11 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -4146,10 +4558,44 @@ snapshots: mkdirp@3.0.1: {} + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + ms@2.1.3: {} + msw@2.11.1(typescript@5.8.3): + dependencies: + '@bundled-es-modules/cookie': 2.0.1 + '@bundled-es-modules/statuses': 1.0.1 + '@inquirer/confirm': 5.1.16 + '@mswjs/interceptors': 0.39.6 + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.6 + graphql: 16.11.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 4.41.0 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - '@types/node' + muggle-string@0.4.1: {} + mute-stream@2.0.0: {} + nano-spawn@1.0.2: {} nanoid@3.3.11: {} @@ -4160,6 +4606,8 @@ snapshots: dependencies: abbrev: 2.0.0 + normalize-path@3.0.0: {} + normalize-wheel-es@1.2.0: {} nth-check@2.1.1: @@ -4199,6 +4647,8 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + outvariant@1.4.3: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -4228,6 +4678,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@6.3.0: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -4249,6 +4701,18 @@ snapshots: optionalDependencies: typescript: 5.8.3 + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + possible-typed-array-names@1.1.0: {} postcss-selector-parser@6.1.2: @@ -4280,12 +4744,24 @@ snapshots: proxy-from-env@1.1.0: {} + punycode@1.4.1: {} + punycode@2.3.1: {} + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + quansync@0.2.11: {} + queue-microtask@1.2.3: {} react-is@17.0.2: {} + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -4295,6 +4771,8 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + require-directory@2.1.1: {} + resolve-from@4.0.0: {} restore-cursor@5.1.0: @@ -4351,6 +4829,8 @@ snapshots: dependencies: xmlchars: 2.2.0 + scule@1.3.0: {} + semver@7.7.2: {} set-function-length@1.2.2: @@ -4423,6 +4903,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.9.0: {} stop-iteration-iterator@1.1.0: @@ -4430,6 +4912,8 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + strict-event-emitter@0.5.1: {} + string-argv@0.3.2: {} string-width@4.2.3: @@ -4510,10 +4994,16 @@ snapshots: tldts-core@6.1.86: {} + tldts-core@7.0.12: {} + tldts@6.1.86: dependencies: tldts-core: 6.1.86 + tldts@7.0.12: + dependencies: + tldts-core: 7.0.12 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -4522,6 +5012,10 @@ snapshots: dependencies: tldts: 6.1.86 + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.12 + tr46@5.1.1: dependencies: punycode: 2.3.1 @@ -4534,12 +5028,84 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.21.3: {} + + type-fest@4.41.0: {} + typescript@5.8.3: {} + ufo@1.6.1: {} + + unimport@5.2.0: + dependencies: + acorn: 8.15.0 + escape-string-regexp: 5.0.0 + estree-walker: 3.0.3 + local-pkg: 1.1.2 + magic-string: 0.30.18 + mlly: 1.8.0 + pathe: 2.0.3 + picomatch: 4.0.3 + pkg-types: 2.3.0 + scule: 1.3.0 + strip-literal: 3.0.0 + tinyglobby: 0.2.14 + unplugin: 2.3.10 + unplugin-utils: 0.2.5 + + unplugin-auto-import@20.1.0(@vueuse/core@9.13.0(vue@3.5.21(typescript@5.8.3))): + dependencies: + local-pkg: 1.1.2 + magic-string: 0.30.18 + picomatch: 4.0.3 + unimport: 5.2.0 + unplugin: 2.3.10 + unplugin-utils: 0.3.0 + optionalDependencies: + '@vueuse/core': 9.13.0(vue@3.5.21(typescript@5.8.3)) + + unplugin-utils@0.2.5: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin-utils@0.3.0: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin-vue-components@29.0.0(@babel/parser@7.28.3)(vue@3.5.21(typescript@5.8.3)): + dependencies: + chokidar: 3.6.0 + debug: 4.4.1 + local-pkg: 1.1.2 + magic-string: 0.30.18 + mlly: 1.8.0 + tinyglobby: 0.2.14 + unplugin: 2.3.10 + unplugin-utils: 0.2.5 + vue: 3.5.21(typescript@5.8.3) + optionalDependencies: + '@babel/parser': 7.28.3 + transitivePeerDependencies: + - supports-color + + unplugin@2.3.10: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.15.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + uri-js@4.4.1: dependencies: punycode: 2.3.1 + url@0.11.4: + dependencies: + punycode: 1.4.1 + qs: 6.14.0 + util-deprecate@1.0.2: {} vite-node@3.2.4(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1): @@ -4577,11 +5143,11 @@ snapshots: lightningcss: 1.30.1 yaml: 2.8.1 - vitest@3.2.4(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(yaml@2.8.1): + vitest@3.2.4(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.11.1(typescript@5.8.3))(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.4(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(msw@2.11.1(typescript@5.8.3))(vite@7.1.4(jiti@2.5.1)(lightningcss@1.30.1)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -4665,6 +5231,8 @@ snapshots: webidl-conversions@7.0.0: {} + webpack-virtual-modules@0.6.2: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 @@ -4712,6 +5280,12 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -4738,8 +5312,24 @@ snapshots: xmlchars@2.2.0: {} + y18n@5.0.8: {} + yallist@5.0.0: {} yaml@2.8.1: {} + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5b23a1c..6c964da 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ onlyBuiltDependencies: - '@tailwindcss/oxide' - esbuild + - msw diff --git a/src/App.vue b/src/App.vue index 3e7078e..91b5e54 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,31 +1,5 @@ - - - - + + \ No newline at end of file diff --git a/src/api/articles.ts b/src/api/articles.ts new file mode 100644 index 0000000..8fe5d72 --- /dev/null +++ b/src/api/articles.ts @@ -0,0 +1,27 @@ +import { http } from '../utils/http.ts' +import type { ArticleResponse, EditArticle } from '../types/articles.ts' + +// 获得你 follow 的用户的最近帖文,query 为获取数量 +interface FollowedFeed {offset?: number, limit?: number} +export const getFollowedFeed = (query?: FollowedFeed, opt?:{signal: AbortSignal}): Promise => http.get('/articles/feed', {params: query, signal: opt?.signal, AuthRequired: true}) + +// 获得最近的全局帖文 +interface GlobalFeed {tag?: string, author?: string, favorited?: string, offset?: number, limit?: number} +export const getGlobalFeed = (query?: GlobalFeed, opt?: {signal?: AbortSignal}): Promise => http.get('/articles', {params: query, signal: opt?.signal}) + +// 创建帖文 +export const createArticle = (data: EditArticle) => http.post('/articles', { article: data }, { AuthRequired: true }); + +// 获取一篇帖文 +export const getArticle = (slug: string) => http.get('/articles/' + slug); + +// 更新帖文 +interface UpdatedArticle { + "title": "string", + "description": "string", + "body": "string" +} +export const updateArticle = (slug: string, data: UpdatedArticle) => http.put('/articles/' + slug, { article: data }, { AuthRequired: true }); + +// 删除帖文 +export const deleteArticle = (slug: string) => http.delete('/delete' + slug, { AuthRequired: true }); diff --git a/src/api/comments.ts b/src/api/comments.ts new file mode 100644 index 0000000..6609416 --- /dev/null +++ b/src/api/comments.ts @@ -0,0 +1,11 @@ +import { http } from '../utils/http.ts' + +// Get comments for a specific article +export const getComment = (slug: string) => http.get(`/articles/${slug}/comments`); + +// Create a comment for an article +interface Comment {body: string} +export const createComment = (slug: string, data: Comment) => http.post(`/articles/${slug}/comments`, { comment: data }, { AuthRequired: true }); + +// Delete a specific comment for an article +export const deleteComment = (slug: string, id: number) => http.delete(`/articles/${slug}/comments/${id}`); \ No newline at end of file diff --git a/src/api/favorites.ts b/src/api/favorites.ts new file mode 100644 index 0000000..dd03b06 --- /dev/null +++ b/src/api/favorites.ts @@ -0,0 +1,8 @@ +import { http } from '../utils/http.ts' + +// Favorite an article +export const likeArticle = (slug: string) => http.post(`/articles/${slug}/favorite`, {AuthRequired: true}); + +// Unfavorite an article +export const dislikeArticle = (slug: string) => http.delete(`/articles/${slug}/favorite`, {AuthRequired: true}) + diff --git a/src/api/profile.ts b/src/api/profile.ts new file mode 100644 index 0000000..2672e6b --- /dev/null +++ b/src/api/profile.ts @@ -0,0 +1,10 @@ +import { http } from '../utils/http.ts' + +// Get a profile of a user +export const getProfile = (username: string) => http.get(`/profiles/${username}`) + +// Follow a user by username +export const followUser = (username: string) => http.post(`/profiles/${username}/follow`, { AuthRequired: true }); + +// Unfollow a user by username +export const unfollowUser = (username: string) => http.delete(`/profiles/${username}/follow`, { AuthRequired: true }); \ No newline at end of file diff --git a/src/api/tags.ts b/src/api/tags.ts new file mode 100644 index 0000000..3488144 --- /dev/null +++ b/src/api/tags.ts @@ -0,0 +1,4 @@ +import { http } from '../utils/http.ts' + +// Get tags +export const getTags = (): Promise> => http.get('/tags'); \ No newline at end of file diff --git a/src/api/users.ts b/src/api/users.ts new file mode 100644 index 0000000..fe39003 --- /dev/null +++ b/src/api/users.ts @@ -0,0 +1,22 @@ +import { http } from '../utils/http.ts' +import type { User, RegisterUser } from '../types/users.ts' + +// Login for existing user +export const getLogin = (data: {username: string, password: string}) => http.post('/users/login', data) + +// Register a user +export const getRegister = (data: {user: RegisterUser}): Promise<{user: User}> => http.post('/users', data) + +// Get the currently logged in user +export const getCurrentUser = () => http.get('/user',{AuthRequired: true}) + +// Update user information for current user +type AtLeastOne = + Keys extends keyof T + ? { [K in Keys]-?: Required> & Partial } + : never + +interface UserUpdatedInfo { + user: AtLeastOne +} +export const updateInfo = (data: UserUpdatedInfo) => http.put('/user', data) diff --git a/src/components/Footer.vue b/src/components/Footer.vue new file mode 100644 index 0000000..2bec889 --- /dev/null +++ b/src/components/Footer.vue @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/src/components/GlobalFeed.vue b/src/components/GlobalFeed.vue new file mode 100644 index 0000000..d63196d --- /dev/null +++ b/src/components/GlobalFeed.vue @@ -0,0 +1,150 @@ + + + diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue deleted file mode 100644 index b58e52b..0000000 --- a/src/components/HelloWorld.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - - - diff --git a/src/components/Home.vue b/src/components/Home.vue new file mode 100644 index 0000000..d054cda --- /dev/null +++ b/src/components/Home.vue @@ -0,0 +1,71 @@ + + + \ No newline at end of file diff --git a/src/components/Layout.vue b/src/components/Layout.vue index 2f7d3e4..2e765de 100644 --- a/src/components/Layout.vue +++ b/src/components/Layout.vue @@ -1,7 +1,13 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/Login.vue b/src/components/Login.vue new file mode 100644 index 0000000..e536bc3 --- /dev/null +++ b/src/components/Login.vue @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/src/components/Me.vue b/src/components/Me.vue new file mode 100644 index 0000000..0323d8d --- /dev/null +++ b/src/components/Me.vue @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/Navbar.vue b/src/components/Navbar.vue new file mode 100644 index 0000000..44c6e73 --- /dev/null +++ b/src/components/Navbar.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/components/NewArticle.vue b/src/components/NewArticle.vue new file mode 100644 index 0000000..94ed2bd --- /dev/null +++ b/src/components/NewArticle.vue @@ -0,0 +1,94 @@ + + + diff --git a/src/components/Register.vue b/src/components/Register.vue new file mode 100644 index 0000000..195c553 --- /dev/null +++ b/src/components/Register.vue @@ -0,0 +1,100 @@ + + + diff --git a/src/components/Setting.vue b/src/components/Setting.vue new file mode 100644 index 0000000..3b6f6f0 --- /dev/null +++ b/src/components/Setting.vue @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/src/components/YourFeed.vue b/src/components/YourFeed.vue new file mode 100644 index 0000000..8a5ad2c --- /dev/null +++ b/src/components/YourFeed.vue @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index f2797ef..2d40178 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,21 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' -import router from './router/index.ts' +import router from './router/index' import './style.css' import App from './App.vue' +import * as ElementPlusIconsVue from "@element-plus/icons-vue"; +import { useAuthStore } from './store/authStore' +import "element-plus/dist/index.css"; const app = createApp(App) const pinia = createPinia() - +for (const [key, val] of Object.entries(ElementPlusIconsVue)) { + app.component(key, val) +} app.use(pinia) -.use(router) -.mount('#app') +app.use(router) +const auth = useAuthStore() +// 这里需要等待bootstrap,防止“先渲染后回跳“的现象 +await auth.bootstrap().catch(() => {}) // 该方法只在此处调用一次,用于刷新整体登录状态 +app.mount('#app') diff --git a/src/router/index.ts b/src/router/index.ts index e935402..0bec5b8 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,15 +1,26 @@ -import {createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router' -import HelloWorld from '../components/HelloWorld.vue' +import { createRouter, createWebHistory } from 'vue-router' +import { routes } from './routes.ts' +import { useAuthStore } from '../store/authStore.ts' -const routes: RouteRecordRaw[] = [ - { - path: '/', - name: 'Home', - component: HelloWorld - } -] const router = createRouter({ history: createWebHistory(), routes }) + +// 全局路由守卫 +router.beforeEach(async to => { + // 只初始化一次,不会在每次路由切换的时候都调用 fetchMe + // 初始化的行为放在 main.ts 里面,只进行一次,这里只通过登录状态来判断逻辑 + const auth = useAuthStore() + if (to.meta.authRequired && !auth.isLoggedIn) { + // 需要权限 && 未登录 + return {name: 'Login', query: {redirect: to.fullPath}} + } + if (to.name === 'Login' && auth.isLoggedIn) { + // 如果目标地址为登录页 && 已经是登录状态,则直接去主页 + return {name: 'GlobalFeed'} + } + return true +}) + export default router \ No newline at end of file diff --git a/src/router/routes.ts b/src/router/routes.ts new file mode 100644 index 0000000..c1d0b76 --- /dev/null +++ b/src/router/routes.ts @@ -0,0 +1,90 @@ +import { type RouteRecordRaw } from 'vue-router'; +import Layout from '../components/Layout.vue'; +export const routes: RouteRecordRaw[] = [ + { + path: "/", + component: Layout, + children: [ + { + path: "", + component: () => import("../components/Home.vue"), + children: [ + { + path: "", + redirect: { name: "GlobalFeed" }, + }, + { + path: "personal-feed", + name: "PersonalFeed", + component: () => import("../components/YourFeed.vue"), + meta: { authRequired: true }, + }, + { + path: "global-feed", + name: "GlobalFeed", + component: () => import("../components/GlobalFeed.vue"), + }, + ], + }, + { + path: "login", + name: "Login", + component: () => import("../components/Login.vue"), + }, + { + path: "register", + name: "Register", + component: () => import("../components/Register.vue"), + }, + { + path: "editor/:slug", + name: "EditArticle", + component: () => import("../components/NewArticle.vue"), + meta: { authRequired: true }, + props: true, + }, + { + path: "editor", + name: "NewArticle", + component: () => import("../components/NewArticle.vue"), + meta: { authRequired: true }, + }, + { + path: "settings", + name: "Settings", + component: () => import("../components/Setting.vue"), + meta: { authRequired: true }, + }, + { + path: "me", + name: "Me", + component: () => import("../components/Me.vue"), + meta: { authRequired: true }, + }, + { + path: "article/:slug", + name: "Article", + component: () => import("../components/Article.vue"), + props: true + }, + { path: ":pathMatch(.*)*", redirect: { name: "GlobalFeed" } } + ], + }, +]; + + +/** + * 路由结构图 +/ +└── Layout.vue (全站外壳:Navbar + Footer) + ├── '' → Home.vue (容器:Hero + Tabs + Popular Tags + ) + │ ├── '' (redirect) → GlobalFeed + │ ├── personal-feed → YourFeed.vue (meta.authRequired = true) + │ └── global-feed → GlobalFeed.vue + │ + ├── login → Login.vue + ├── register → Register.vue + ├── edit/:slug → NewArticle.vue (meta.authRequired = true) + ├── settings → Setting.vue (meta.authRequired = true) + └── me → Me.vue (meta.authRequired = true) + */ \ No newline at end of file diff --git a/src/store/articleStore.ts b/src/store/articleStore.ts new file mode 100644 index 0000000..1499e8a --- /dev/null +++ b/src/store/articleStore.ts @@ -0,0 +1,197 @@ +import { defineStore } from 'pinia' +import { getGlobalFeed, getFollowedFeed } from '../api/articles' +import { reactive } from 'vue' +import type { Article } from '../types/articles' +import axios from 'axios' + +// 这是一个多列表容器 +interface ListState { + slugs: string[]; // 只存引用,对应的数据在 bySlug + total: number; // 总条数 + error: string | null; + params: Record; // 当前这个列表的查询参数快照(tag/author/limit/offset…) + loading: boolean; + abort?: AbortController; // 取消在途请求(切换筛选/分页时用) + // store 内部的“开关”,保证你切换参数时不会让旧请求回写覆盖数据。// lastUpdated: number; +} +const normalizeError = (e: unknown) => { + // 这里是根据 API 文档中的错误数据的结构类型写的 + return (e as any)?.message || (e as any)?.response?.data?.errors?.body[0] || 'Request failed!' +} + +/** + * + * @param params + * @returns + * 把查询参数稳定序列化为 key。 + */ +const keyOf = (params: Record) => { + const usp = new URLSearchParams() + const keys = Object.keys(params).sort() + for (const k of keys) { + const val = params[k] + if (val === null || val === undefined || val === '') continue + usp.append(k, String(val)) + } + return usp.toString() || '__default__' +} +export const useArticleStore = defineStore('article', () => { + // 将每篇 article 缓存在这个字典里 + const bySlug = reactive>({}); + + // 将 articles 使用 slug 进行映射 + const writeEntities = (articles: Article[]) => { + for (const a of articles) { + bySlug[a.slug] = a; + } + }; + const globalLists = reactive>({}); // key 为序列化之后的查询参数 + const feedLists = reactive>({}); + + /** + * + * @param map + * @param params + * @returns + * 保证某个 key 的 ListState 存在(不存在就初始化),返回 { key, state } + */ + const ensure = ( + map: Record, + params: Record + ) => { + // 这个工具函数是确保在没有传 params 的时候给定一个默认的对象 + // 这里的 map 即 globalLists / feedLists + const key = keyOf(params); + if (!map[key]) { + map[key] = { slugs: [], total: 0, loading: false, params: { ...params }, error: null}; + } + return { key, state: map[key] }; + }; + + /** + * + * @param params + * 这是覆盖式的,同名会覆盖 + */ + async function fetchGlobal(params: Record) { + const { key, state } = ensure(globalLists, params); + state.loading = true; + // 此处的 loading 是否为响应式的? + state.error = null + + // 取消上一次的请求,绑定新的取消控制器 + state.abort?.abort() + const ac = new AbortController() + state.abort = ac + try { + const res = await getGlobalFeed(params, {signal: ac.signal}); + writeEntities(res.articles); + state.slugs = res.articles.map((a) => a.slug); + state.total = res.articlesCount; + return { key, ok: true }; + } catch (e) { + if (axios.isAxiosError(e) && (e as any).code === "ERR_CANCELED") return + if ((e as any).name === 'AbortError') return + state.error = normalizeError(e) + return { key, ok: false }; + } finally { + if (!ac.signal.aborted) { + state.loading = false; + } + } + } + async function fetchFeed(params: Record) { + const { key, state } = ensure(feedLists, params); + state.loading = true; + state.error = null; + + // 取消旧的请求,添加新的取消控制器 + state.abort?.abort(); + const ac = new AbortController() + state.abort = ac + try { + const res = await getFollowedFeed(params, {signal: ac.signal}); + writeEntities(res.articles); + state.slugs = res.articles.map((a) => a.slug); + state.total = res.articlesCount; + return { key, ok: true}; + } catch (e) { + // 如果错误类型为 AbortError ,那么将会触发新的一次请求,没有必要处理这个错误 + if (axios.isAxiosError(e) && e.code === "ERR_CANCELED") { + // axios 的取消,是 CanceledError,code 为 ERR_CANCELED + return + } + if ((e as any).name === 'AbortError') { + // 有的环境是这个错误 + return + } + state.error = normalizeError(e); + return { key, ok: false}; + } finally { + // 只有当"这一次"请求没有被取消,才把 loading 置为 false + if (!ac.signal.aborted) { + state.loading = false; + } + } + } + function selectGlobal(params: Record): ListState { + return ensure(globalLists, params).state + } + + + function selectFeed(params: Record): ListState { + return ensure(feedLists, params).state + } + + /** + * + * @param slug + * @returns + * 从实体缓存拿文章。 + */ + function getArticle(slug: string) { + return bySlug[slug] || null; + } + + async function appendGlobal(params: Record) { + const { key, state } = ensure(globalLists, params) + if (state.loading) return { key, ok: true } // 防止重复触发 + + state.loading = true + state.error = null + state.abort?.abort() + const ac = new AbortController() + state.abort = ac + try { + const res = await getGlobalFeed(params, {signal: ac.signal}) + writeEntities(res.articles) + const more = res.articles.map(a => a.slug) + const set = new Set([...state.slugs, ...more]) + state.slugs = Array.from(set) + state.total = res.articlesCount + return { key, ok: true } + } catch (e) { + if (axios.isAxiosError(e) && (e as any).code === "ERR_CANCELED") return; + if ((e as any).name === "AbortError") return; + state.error = normalizeError(e) + return { key, ok: false } + } finally { + if (!ac.signal.aborted) { + state.loading = false; + } + } + } + return { + // states + globalLists, + feedLists, + // actions + fetchGlobal, + fetchFeed, + appendGlobal, + // selectors + selectFeed, + selectGlobal, + getArticle, + }; +}) \ No newline at end of file diff --git a/src/store/authStore.ts b/src/store/authStore.ts new file mode 100644 index 0000000..1d5a855 --- /dev/null +++ b/src/store/authStore.ts @@ -0,0 +1,140 @@ +// authStore.ts +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' +import { getCurrentUser, getLogin, getRegister } from '../api/users' +import { getToken, setToken, removeToken } from '../utils/token.ts' +import type { User, RegisterUser } from '../types/users.ts' +import axios from 'axios' + +export const useAuthStore = defineStore('auth', () => { + const token = ref(getToken()); + const user = ref(null); + const isLoggedIn = computed(() => !!token.value); + + // 会话级状态机 + const status = ref<"idle" | "loading" | "authenticated">("idle"); + /** + * 会话级别的状态(登录/认证流程) → 用 authStore.status 或它的衍生计算属性。 + * 组件自身的业务 loading → 自己维护 ref。 + */ + const booted = ref(false); + // 登录:更新状态并在失败时抛出原始错误 + async function login(payload: { + username: string; + password: string; + }): Promise { + status.value = "loading"; + try { + const res = await getLogin(payload); + // http 层已做解包,直接从 data 中取 user + user.value = (res as any)?.user ?? null; + token.value = user.value?.token ?? null; + // 此处应该在确定有 token 的时候将 status 置为 authenticated + // 最初版本没要考虑到这一点,此为后续补充 + if (token.value) { + setToken(token.value); + status.value = "authenticated"; + } else { + status.value = "idle"; + } + } catch (e) { + status.value = "idle"; + throw e; + } + } + /** + * 在登录逻辑里面,为了防止用户狂按 登录 ,可以利用 status 的 loading 这个全局状态对按钮进行限制,这个应该放在组件里面操作 + * + */ + + // 注册 + async function register(payload: {user: RegisterUser}): Promise { + status.value = 'loading' + try { + const res = await getRegister(payload) + user.value = res.user || null + token.value = user.value?.token ?? null + if (token.value) { + setToken(token.value) + status.value = 'authenticated' + } else { + status.value = 'idle' + } + } catch (e) { + status.value = 'idle' + throw e + } + } + + // 拉取当前用户:401 时执行登出并抛出错误 + async function fetchMe(): Promise { + if (!token.value) return; + try { + const res = await getCurrentUser(); + user.value = (res as any)?.user ?? null; + if (user.value?.token) { + token.value = user.value.token; + setToken(user.value.token); + status.value = "authenticated"; + } else { + token.value = null + removeToken() + status.value = 'idle' + } + } catch (e) { + // 这里只捕获 401 错误,其他错误在个子组件里面自行捕获处理 + if (axios.isAxiosError(e) && e.response?.status === 401) { + // 这里的 401 属于 HTTP 层级的语义 + logout(); + } + throw e; + } + } + + // 启动:有 token 则验证当前会话,失败时清理并将错误继续抛出 + async function bootstrap(): Promise { + if (token.value) { + try { + await fetchMe(); + } catch (e) { + logout(); + throw e; + } + } + booted.value = true; + } + + function logout(): void { + token.value = null; + user.value = null; + removeToken(); + status.value = "idle"; + } + + return { + token, + user, + isLoggedIn, + status, + booted, + login, + fetchMe, + logout, + bootstrap, + register + }; +}) +/** + * booted 字段使用场景: + * 路由守卫:有些页面要等 booted 为 true 才能正确判断是否放行。 + * 避免“闪屏”:比如刚启动时,虽然 status 可能还是 "idle",但实际上还没跑 fetchMe(),不能贸然认为“未登录”。 + * 它是 一次性开关,初始化完成后固定为 true。 + */ + +/** + * booted / status是否有必要同时存在? + • 有必要。 + • status 负责表达“会话当前状态”,但它没法告诉你“authStore 是否初始化过”。 + • 举例:应用启动时,status 可能是 "idle",但这并不意味着用户一定没登录,有可能 token 在本地,要等 fetchMe() 校验才知道。 + • booted 就解决了这个问题,它告诉你“认证初始化流程是否跑完”,保证路由守卫和 UI 能安全地用 status 去判断。 + */ \ No newline at end of file diff --git a/src/store/user.ts b/src/store/userStore.ts similarity index 100% rename from src/store/user.ts rename to src/store/userStore.ts diff --git a/src/style.css b/src/style.css index 818f283..a461c50 100644 --- a/src/style.css +++ b/src/style.css @@ -1,80 +1 @@ -@import "tailwindcss"; -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -.card { - padding: 2em; -} - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} +@import "tailwindcss"; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..60300b0 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,13 @@ +export interface Article { + id: number; + author: { + username: string; + avatar: string; + }; + createdAt: string; + title: string; + description: string; + tags: string[]; + favoritesCount: number; + favorited: boolean; +} diff --git a/src/types/articles.ts b/src/types/articles.ts new file mode 100644 index 0000000..ad5983b --- /dev/null +++ b/src/types/articles.ts @@ -0,0 +1,21 @@ +interface Author { + username: string; + bio: string; + image: string; + following: boolean; +} +export interface Article { + slug: string; + title: string; + description: string; + tagList: string[]; + createdAt: string; // ISO 时间字符串 + updatedAt: string; // ISO 时间字符串 + favorited: boolean; + favoritesCount: number; + author: Author; +} +export interface ArticleResponse { + articles: Article[]; + articlesCount: number; +} \ No newline at end of file diff --git a/src/types/users.ts b/src/types/users.ts new file mode 100644 index 0000000..8216b6b --- /dev/null +++ b/src/types/users.ts @@ -0,0 +1,13 @@ +// 当前的用户信息 +export interface User { + email: string; + token: string; + username: string; + bio: string; + image: string; +} +export interface RegisterUser { + username: string, + email: string, + password: string +} \ No newline at end of file diff --git a/src/utils/http.ts b/src/utils/http.ts new file mode 100644 index 0000000..ff3ed5a --- /dev/null +++ b/src/utils/http.ts @@ -0,0 +1,46 @@ +import axios, { AxiosHeaders, type AxiosRequestConfig } from 'axios' +import { isRef } from 'vue' +import { useAuthStore } from '../store/authStore' + +// 为 axios 增加类型声明 +declare module 'axios' { + export interface AxiosRequestConfig { + // 在调用 axios interceptor 的时候,传入这个字段标识是否需要鉴权 + AuthRequired?: boolean + } +} + +/** + * Axios 的两个“层” + * HTTP 层:真实的 HTTP 状态码(response.status,如 200/401/500…)。 + * 业务层:后端放在 响应体里 的自定义码(response.data.code,如 0/200/401/501…)。 + */ + +export const http = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL, + timeout: 15000 +}) + +http.interceptors.request.use(config => { + if (config.AuthRequired) { + // 如果这个文件在 Pinia 初始化前被 import,会拿不到 store。所以不要在模块顶层使用 useAuthStore() + const { token: tokenSource } = useAuthStore() + const token = isRef(tokenSource) ? tokenSource.value : tokenSource + if (token) { + // 有 token ,则把 token 挂到 请求体的 header 上面 + config.headers = new AxiosHeaders(config.headers) + config.headers.set( 'Authorization', `Token ${token}`) + } + } + delete config.AuthRequired + return config +}, error => { + return Promise.reject(error) +}) + +http.interceptors.response.use(response => { + // HTTP 200 + return response.data +}, error => { + return Promise.reject(error) +}) diff --git a/src/utils/token.ts b/src/utils/token.ts new file mode 100644 index 0000000..ab9dee4 --- /dev/null +++ b/src/utils/token.ts @@ -0,0 +1,13 @@ +const KEY = 'userToken' + +export const setToken = (token: string): void => { + localStorage.setItem(KEY, token) +} + +export const removeToken = (): void => { + localStorage.removeItem(KEY) +} + +export const getToken = (): string | null => { + return localStorage.getItem(KEY) +} \ No newline at end of file diff --git a/src/smoke.test.ts b/test/smoke.test.ts similarity index 100% rename from src/smoke.test.ts rename to test/smoke.test.ts diff --git a/test/user.test.ts b/test/user.test.ts new file mode 100644 index 0000000..cb2ed12 --- /dev/null +++ b/test/user.test.ts @@ -0,0 +1,25 @@ +import axios from "axios" + + +test('get the current user', async () => { + // Given + const currentUser = { + email: "xxx@gmail.com", + token: "TEST", + username: "xxx", + bio: "bio desc", + image: "img", + } + + // When + const res = fetch('http://localhost:5173/user') + + // Then + await expect((await res).json()).resolves.toEqual(currentUser); +}) +interface User { + email: string; + password: string; + username: string; +} + diff --git a/tsconfig.app.json b/tsconfig.app.json index 6bfd5d2..952937c 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -1,6 +1,7 @@ { "extends": "@vue/tsconfig/tsconfig.dom.json", "compilerOptions": { + "composite": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "types": ["vitest/globals"], diff --git a/tsconfig.json b/tsconfig.json index 1ffef60..e4a145f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,26 @@ { - "files": [], + // "files": [], + // files:用于精确列出要编译的文件。你明确告诉 TypeScript 需要编译哪些文件。 + "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } - ] + ], + + "include": ["src/**/*", "test/**/*"], + // include:用于指定文件夹或者通配符模式,表示哪些文件需要被 TypeScript 编译。 + + "compilerOptions": { + "moduleResolution": "node", // 使用 Node.js 模块解析 + // 用于定义编译器如何处理你的代码。 + "types": ["vitest/globals"], + // types 字段用来指定哪些全局类型声明应该被加载。 + // 告知 TypeScript 使用 vitest 提供的全局类型定义,从而使得 describe、it、expect 等函数可以在测试文件中全局可用。 + "target": "ES2015", + "lib": ["ES2015", "DOM"], // 引入 ES2015 和 DOM 库 + "baseUrl": ".", // 很重要 + "paths": { + "@/*": ["src/*"] // 让 @ 指向 src + } + } } diff --git a/tsconfig.node.json b/tsconfig.node.json index f85a399..4b917c1 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,8 +1,9 @@ { "compilerOptions": { + "composite": true, "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "target": "ES2023", - "lib": ["ES2023"], + "lib": ["ES2023", "ES2015"], "module": "ESNext", "skipLibCheck": true, diff --git a/vite.config.ts b/vite.config.ts index 9f6a31d..8ee337f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,35 @@ import { defineConfig } from 'vitest/config' import tailwindcss from "@tailwindcss/vite"; import vue from '@vitejs/plugin-vue' +import Components from "unplugin-vue-components/vite"; +import { ElementPlusResolver } from "unplugin-vue-components/resolvers"; // https://vite.dev/config/ export default defineConfig({ - plugins: [vue(), tailwindcss()], + plugins: [ + vue(), + tailwindcss(), + Components({ + resolvers: [ElementPlusResolver()], // 自动导入 Element Plus 组件 + }), + ], test: { + setupFiles: "./vitest.setup.ts", globals: true, // 允许 describe/it/expect 全局可用 - environment: "jsdom", // Vue 组件测试需要 DOM 环境 + environment: "node", // Vue 组件测试需要 DOM 环境, mock 接口测试需要 node 环境 + coverage: { + reporter: ["text", "json", "html"], // 配置代码覆盖率报告 + }, + include: ["test/**/*.test.ts"], + }, + server: { + proxy: { + "/api": { + target: "https://api.realworld.show/api", + changeOrigin: true, + // rewrite: (path) => path.replace(/^\/api/, "/api"), + }, + }, }, }); /** diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000..076d7fb --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,18 @@ +import { beforeAll, afterEach, afterAll } from "vitest" +import { server } from "./mock/node.ts" + +beforeAll(() => { + // 启动 MSW 服务器并监听 HTTP 请求,在测试开始前,它会拦截所有网络请求 + server.listen() + console.log('正在监听所有测试。。。') +}) + +afterEach(() => { + // 每次测试执行后重置所有已设置的请求处理器,以便不影响后续测试 + server.resetHandlers() + console.log('确保每个测试后都重置 MSW 请求处理器,防止测试间有状态或处理器的干扰') +}) +afterAll(() => { +// 停止 MSW 服务器,释放资源,防止资源泄漏 + server.close() +})