diff --git a/LICENSE b/packages/core/LICENSE similarity index 100% rename from LICENSE rename to packages/core/LICENSE diff --git a/packages/core/package.json b/packages/core/package.json index cc2df5c..ab16474 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,7 +13,6 @@ "pridepack" ], "devDependencies": { - "@types/negotiator": "^0.6.3", "@types/node": "^20.11.30", "pridepack": "2.6.0", "tslib": "^2.6.2", @@ -45,7 +44,6 @@ "access": "public" }, "dependencies": { - "negotiator": "^0.6.3", "tsx": "^4.7.1", "valibot": "^0.30.0" }, diff --git a/packages/core/src/backend/core.ts b/packages/core/src/backend/core.ts index 7b5155f..e7e31f2 100644 --- a/packages/core/src/backend/core.ts +++ b/packages/core/src/backend/core.ts @@ -43,12 +43,12 @@ export const createBackend = (params: CreateBackendParams) => { backendState.checksSerializersOnDelete = b; return this; }, - createServer(_type: string, _options = {}): Server { + createServer(_type: string, _options = {}) { return { requestDecorator() { return this; }, - } satisfies Server; + } as unknown as T; }, use(extender) { return extender(backendState, this); diff --git a/packages/core/src/backend/index.ts b/packages/core/src/backend/index.ts index 126256a..1243f85 100644 --- a/packages/core/src/backend/index.ts +++ b/packages/core/src/backend/index.ts @@ -1,6 +1,3 @@ export * from './core'; export * from './common'; export * from './data-source'; - -// where to put these? we should be publishing them as a separate entry point -export * as http from '../servers/http'; diff --git a/packages/core/src/common/media-type.ts b/packages/core/src/common/media-type.ts index 9abd4ee..be412ae 100644 --- a/packages/core/src/common/media-type.ts +++ b/packages/core/src/common/media-type.ts @@ -25,3 +25,13 @@ export type PatchContentType = typeof PATCH_CONTENT_TYPES[number]; export const getAcceptPostString = (mediaTypes: Map) => Array.from(mediaTypes.keys()) .filter((t) => !PATCH_CONTENT_TYPES.includes(t as PatchContentType)) .join(','); + +export const isTextMediaType = (mediaType: string) => ( + mediaType.startsWith('text/') + || [ + 'application/json', + 'application/xml', + 'application/x-www-form-urlencoded', + ...PATCH_CONTENT_TYPES, + ].includes(mediaType) +); diff --git a/packages/data-sources/duckdb/package.json b/packages/data-sources/duckdb/package.json index 7116328..8f62054 100644 --- a/packages/data-sources/duckdb/package.json +++ b/packages/data-sources/duckdb/package.json @@ -20,7 +20,7 @@ "vitest": "^1.2.0" }, "dependencies": { - "@modal-sh/yasumi": "*", + "@modal-sh/yasumi": "workspace:*", "duckdb-async": "^0.10.0" }, "scripts": { diff --git a/packages/data-sources/file-jsonl/package.json b/packages/data-sources/file-jsonl/package.json index 855989b..9097bcf 100644 --- a/packages/data-sources/file-jsonl/package.json +++ b/packages/data-sources/file-jsonl/package.json @@ -30,7 +30,7 @@ "test": "vitest" }, "dependencies": { - "@modal-sh/yasumi": "*" + "@modal-sh/yasumi": "workspace:*" }, "private": false, "description": "JSON lines file data source for yasumi.", diff --git a/packages/examples/cms-web-api/package.json b/packages/examples/cms-web-api/package.json index 31f45e3..afd5b93 100644 --- a/packages/examples/cms-web-api/package.json +++ b/packages/examples/cms-web-api/package.json @@ -19,8 +19,9 @@ "vitest": "^1.2.0" }, "dependencies": { - "@modal-sh/yasumi": "*", - "@modal-sh/yasumi-data-source-file-jsonl": "*", + "@modal-sh/yasumi": "workspace:*", + "@modal-sh/yasumi-server-http": "workspace:*", + "@modal-sh/yasumi-data-source-file-jsonl": "workspace:*", "tsx": "^4.7.1" }, "scripts": { diff --git a/packages/examples/cms-web-api/src/index.ts b/packages/examples/cms-web-api/src/index.ts index 05ec750..79b7020 100644 --- a/packages/examples/cms-web-api/src/index.ts +++ b/packages/examples/cms-web-api/src/index.ts @@ -1,5 +1,5 @@ import { application, resource, validation as v } from '@modal-sh/yasumi'; -import { http } from '@modal-sh/yasumi/backend'; +import * as http from '@modal-sh/yasumi-server-http'; import { randomUUID } from 'crypto'; import { JsonLinesDataSource } from '@modal-sh/yasumi-data-source-file-jsonl'; import { constants } from 'http2'; diff --git a/packages/examples/duckdb/package.json b/packages/examples/duckdb/package.json index 6617e3a..1329b4e 100644 --- a/packages/examples/duckdb/package.json +++ b/packages/examples/duckdb/package.json @@ -19,8 +19,9 @@ "vitest": "^1.2.0" }, "dependencies": { - "@modal-sh/yasumi": "*", - "@modal-sh/yasumi-data-source-duckdb": "*", + "@modal-sh/yasumi": "workspace:*", + "@modal-sh/yasumi-server-http": "workspace:*", + "@modal-sh/yasumi-data-source-duckdb": "workspace:*", "tsx": "^4.7.1" }, "scripts": { diff --git a/packages/examples/duckdb/src/index.ts b/packages/examples/duckdb/src/index.ts index 9d76edf..76e8600 100644 --- a/packages/examples/duckdb/src/index.ts +++ b/packages/examples/duckdb/src/index.ts @@ -1,5 +1,5 @@ import { resource, application, validation as v } from '@modal-sh/yasumi'; -import { http } from '@modal-sh/yasumi/backend'; +import * as http from '@modal-sh/yasumi-server-http'; import { DuckDbDataSource, AutoincrementIdConfig } from '@modal-sh/yasumi-data-source-duckdb'; import { constants } from 'http2'; @@ -29,10 +29,11 @@ const app = application({ const backend = app.createBackend({ dataSource: new DuckDbDataSource('test.db'), }) + .use(http.httpExtender) .showTotalItemCountOnGetCollection() .throwsErrorOnDeletingNotFound(); -const server = backend.createHttpServer({ +const server = backend.createServer('http', { basePath: '/api', }) .defaultErrorHandler((_req, res) => () => { diff --git a/packages/servers/http/.gitignore b/packages/servers/http/.gitignore new file mode 100644 index 0000000..53992de --- /dev/null +++ b/packages/servers/http/.gitignore @@ -0,0 +1,107 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.production +.env.development + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +.npmrc diff --git a/packages/servers/http/LICENSE b/packages/servers/http/LICENSE new file mode 100644 index 0000000..5a4fdfd --- /dev/null +++ b/packages/servers/http/LICENSE @@ -0,0 +1,7 @@ +MIT License Copyright (c) 2024 TheoryOfNekomata + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/servers/http/package.json b/packages/servers/http/package.json new file mode 100644 index 0000000..64665f9 --- /dev/null +++ b/packages/servers/http/package.json @@ -0,0 +1,68 @@ +{ + "name": "@modal-sh/yasumi-server-http", + "version": "0.0.0", + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=16" + }, + "license": "MIT", + "keywords": [ + "pridepack" + ], + "devDependencies": { + "@types/negotiator": "^0.6.3", + "@types/node": "^20.11.0", + "pridepack": "2.6.0", + "tslib": "^2.6.2", + "typescript": "^5.3.3", + "vitest": "^1.2.0" + }, + "dependencies": { + "@modal-sh/yasumi": "workspace:*", + "negotiator": "^0.6.3" + }, + "scripts": { + "prepublishOnly": "pridepack clean && pridepack build", + "build": "pridepack build", + "type-check": "pridepack check", + "clean": "pridepack clean", + "watch": "pridepack watch", + "start": "pridepack start", + "dev": "pridepack dev", + "test": "vitest" + }, + "private": false, + "description": "HTTP server for Yasumi backend.", + "repository": { + "url": "", + "type": "git" + }, + "homepage": "", + "bugs": { + "url": "" + }, + "author": "TheoryOfNekomata ", + "publishConfig": { + "access": "public" + }, + "types": "./dist/types/index.d.ts", + "main": "./dist/cjs/production/index.js", + "module": "./dist/esm/production/index.js", + "exports": { + ".": { + "development": { + "require": "./dist/cjs/development/index.js", + "import": "./dist/esm/development/index.js" + }, + "require": "./dist/cjs/production/index.js", + "import": "./dist/esm/production/index.js", + "types": "./dist/types/index.d.ts" + } + }, + "typesVersions": { + "*": {} + } +} diff --git a/packages/servers/http/pridepack.json b/packages/servers/http/pridepack.json new file mode 100644 index 0000000..0bc7a8f --- /dev/null +++ b/packages/servers/http/pridepack.json @@ -0,0 +1,3 @@ +{ + "target": "es2018" +} diff --git a/packages/core/src/servers/http/core.ts b/packages/servers/http/src/core.ts similarity index 99% rename from packages/core/src/servers/http/core.ts rename to packages/servers/http/src/core.ts index 7371eee..507e746 100644 --- a/packages/core/src/servers/http/core.ts +++ b/packages/servers/http/src/core.ts @@ -1,7 +1,6 @@ import http, { createServer as httpCreateServer } from 'http'; import { createServer as httpCreateSecureServer } from 'https'; import {constants,} from 'http2'; -import * as v from 'valibot'; import EventEmitter from 'events'; import { AllowedMiddlewareSpecification, @@ -12,10 +11,8 @@ import { RequestDecorator, Response, Server, -} from '../../backend/common'; -import { - DataSource -} from '../../backend/data-source'; + DataSource, +} from '@modal-sh/yasumi/backend'; import { BaseResourceType, CanPatchSpec, @@ -26,7 +23,9 @@ import { PATCH_CONTENT_MAP_TYPE, PATCH_CONTENT_TYPES, PatchContentType, queryMediaTypes, Resource, -} from '../../common'; + validation as v, + isTextMediaType, +} from '@modal-sh/yasumi'; import { handleGetRoot, handleOptions, } from './handlers/default'; @@ -39,7 +38,7 @@ import { handlePatchItem, handleQueryCollection, } from './handlers/resource'; -import {getBody, isTextMediaType} from './utils'; +import {getBody} from './utils'; import {decorateRequestWithBackend} from './decorators/backend'; import {decorateRequestWithMethod} from './decorators/method'; import {decorateRequestWithUrl} from './decorators/url'; @@ -63,7 +62,7 @@ export interface HttpServer extends Server { defaultErrorHandler(errorHandler: ErrorHandler): this; } -declare module '../../backend' { +declare module '@modal-sh/yasumi/backend' { interface RequestContext extends http.IncomingMessage { body?: unknown; } diff --git a/packages/core/src/servers/http/decorators/backend/content-negotiation.ts b/packages/servers/http/src/decorators/backend/content-negotiation.ts similarity index 86% rename from packages/core/src/servers/http/decorators/backend/content-negotiation.ts rename to packages/servers/http/src/decorators/backend/content-negotiation.ts index dcffaa7..cc65c35 100644 --- a/packages/core/src/servers/http/decorators/backend/content-negotiation.ts +++ b/packages/servers/http/src/decorators/backend/content-negotiation.ts @@ -1,8 +1,8 @@ -import {ContentNegotiation} from '../../../../common'; -import {RequestDecorator} from '../../../../backend/common'; +import {ContentNegotiation} from '@modal-sh/yasumi'; +import {RequestDecorator} from '@modal-sh/yasumi/backend'; import Negotiator from 'negotiator'; -declare module '../../../../backend/common' { +declare module '@modal-sh/yasumi/backend' { interface RequestContext { cn: Partial; } diff --git a/packages/core/src/servers/http/decorators/backend/index.ts b/packages/servers/http/src/decorators/backend/index.ts similarity index 76% rename from packages/core/src/servers/http/decorators/backend/index.ts rename to packages/servers/http/src/decorators/backend/index.ts index a20178e..b51acdd 100644 --- a/packages/core/src/servers/http/decorators/backend/index.ts +++ b/packages/servers/http/src/decorators/backend/index.ts @@ -1,8 +1,8 @@ -import {BackendState, ParamRequestDecorator} from '../../../../backend/common'; +import {BackendState, ParamRequestDecorator} from '@modal-sh/yasumi/backend'; import {decorateRequestWithContentNegotiation} from './content-negotiation'; import {decorateRequestWithResource} from './resource'; -declare module '../../../../backend/common' { +declare module '@modal-sh/yasumi/backend' { interface RequestContext { backend: BackendState; } diff --git a/packages/core/src/servers/http/decorators/backend/resource.ts b/packages/servers/http/src/decorators/backend/resource.ts similarity index 79% rename from packages/core/src/servers/http/decorators/backend/resource.ts rename to packages/servers/http/src/decorators/backend/resource.ts index 7ccfc32..63df76e 100644 --- a/packages/core/src/servers/http/decorators/backend/resource.ts +++ b/packages/servers/http/src/decorators/backend/resource.ts @@ -1,7 +1,7 @@ -import {Resource} from '../../../../common'; -import {RequestDecorator} from '../../../../backend/common'; +import {Resource} from '@modal-sh/yasumi'; +import {RequestDecorator} from '@modal-sh/yasumi/backend'; -declare module '../../../../backend/common' { +declare module '@modal-sh/yasumi/backend' { interface RequestContext { resource?: Resource; resourceId?: string; diff --git a/packages/core/src/servers/http/decorators/method/index.ts b/packages/servers/http/src/decorators/method/index.ts similarity index 91% rename from packages/core/src/servers/http/decorators/method/index.ts rename to packages/servers/http/src/decorators/method/index.ts index f39778a..6a81adb 100644 --- a/packages/core/src/servers/http/decorators/method/index.ts +++ b/packages/servers/http/src/decorators/method/index.ts @@ -1,4 +1,4 @@ -import {RequestDecorator} from '../../../../backend/common'; +import {RequestDecorator} from '@modal-sh/yasumi/backend'; const METHOD_SPOOF_HEADER_NAME = 'x-original-method' as const; const METHOD_SPOOF_ORIGINAL_METHOD = 'POST' as const; diff --git a/packages/core/src/servers/http/decorators/url/base-path.ts b/packages/servers/http/src/decorators/url/base-path.ts similarity index 63% rename from packages/core/src/servers/http/decorators/url/base-path.ts rename to packages/servers/http/src/decorators/url/base-path.ts index 7a0274c..b101f09 100644 --- a/packages/core/src/servers/http/decorators/url/base-path.ts +++ b/packages/servers/http/src/decorators/url/base-path.ts @@ -1,6 +1,6 @@ -import {ParamRequestDecorator} from '../../../../backend/common'; +import {ParamRequestDecorator} from '@modal-sh/yasumi/backend'; -declare module '../../../../backend/common' { +declare module '@modal-sh/yasumi/backend' { interface RequestContext { basePath: string; } diff --git a/packages/core/src/servers/http/decorators/url/host.ts b/packages/servers/http/src/decorators/url/host.ts similarity index 61% rename from packages/core/src/servers/http/decorators/url/host.ts rename to packages/servers/http/src/decorators/url/host.ts index 465dc84..9901dca 100644 --- a/packages/core/src/servers/http/decorators/url/host.ts +++ b/packages/servers/http/src/decorators/url/host.ts @@ -1,6 +1,6 @@ -import {ParamRequestDecorator} from '../../../../backend/common'; +import {ParamRequestDecorator} from '@modal-sh/yasumi/backend'; -declare module '../../../../backend/common' { +declare module '@modal-sh/yasumi/backend' { interface RequestContext { host: string; } diff --git a/packages/core/src/servers/http/decorators/url/index.ts b/packages/servers/http/src/decorators/url/index.ts similarity index 89% rename from packages/core/src/servers/http/decorators/url/index.ts rename to packages/servers/http/src/decorators/url/index.ts index efe4f0c..6d472cc 100644 --- a/packages/core/src/servers/http/decorators/url/index.ts +++ b/packages/servers/http/src/decorators/url/index.ts @@ -1,10 +1,10 @@ -import {ParamRequestDecorator} from '../../../../backend/common'; +import {ParamRequestDecorator} from '@modal-sh/yasumi/backend'; import {CreateServerParams} from '../../core'; import {decorateRequestWithScheme} from './scheme'; import {decorateRequestWithHost} from './host'; import {decorateRequestWithBasePath} from './base-path'; -declare module '../../../../backend/common' { +declare module '@modal-sh/yasumi/backend' { interface RequestContext { rawUrl?: string; query: URLSearchParams; diff --git a/packages/core/src/servers/http/decorators/url/scheme.ts b/packages/servers/http/src/decorators/url/scheme.ts similarity index 62% rename from packages/core/src/servers/http/decorators/url/scheme.ts rename to packages/servers/http/src/decorators/url/scheme.ts index 3e92c70..cb02d60 100644 --- a/packages/core/src/servers/http/decorators/url/scheme.ts +++ b/packages/servers/http/src/decorators/url/scheme.ts @@ -1,6 +1,6 @@ -import {ParamRequestDecorator} from '../../../../backend/common'; +import {ParamRequestDecorator} from '@modal-sh/yasumi/backend'; -declare module '../../../../backend/common' { +declare module '@modal-sh/yasumi/backend' { interface RequestContext { scheme: string; } diff --git a/packages/core/src/servers/http/handlers/default.ts b/packages/servers/http/src/handlers/default.ts similarity index 94% rename from packages/core/src/servers/http/handlers/default.ts rename to packages/servers/http/src/handlers/default.ts index 2717c2e..657f2f5 100644 --- a/packages/core/src/servers/http/handlers/default.ts +++ b/packages/servers/http/src/handlers/default.ts @@ -1,8 +1,8 @@ import {constants} from 'http2'; -import {AllowedMiddlewareSpecification, getAllowString, Middleware} from '../../../backend/common'; +import {AllowedMiddlewareSpecification, getAllowString, Middleware} from '@modal-sh/yasumi/backend'; import {LinkMap} from '../utils'; import {PlainResponse, ErrorPlainResponse} from '../response'; -import {getAcceptPatchString, getAcceptPostString} from '../../../common'; +import {getAcceptPatchString, getAcceptPostString} from '@modal-sh/yasumi'; export const handleGetRoot: Middleware = (req, res) => { const { backend, basePath } = req; diff --git a/packages/core/src/servers/http/handlers/resource.ts b/packages/servers/http/src/handlers/resource.ts similarity index 99% rename from packages/core/src/servers/http/handlers/resource.ts rename to packages/servers/http/src/handlers/resource.ts index d5a8524..82abbe0 100644 --- a/packages/core/src/servers/http/handlers/resource.ts +++ b/packages/servers/http/src/handlers/resource.ts @@ -1,14 +1,14 @@ import { constants } from 'http2'; -import * as v from 'valibot'; import assert from 'assert'; -import {Middleware} from '../../../backend/common'; +import {Middleware} from '@modal-sh/yasumi/backend'; import { applyDelta, Query, Delta, PATCH_CONTENT_MAP_TYPE, PatchContentType, queryMediaTypes, -} from '../../../common'; + validation as v, +} from '@modal-sh/yasumi'; import {ErrorPlainResponse, PlainResponse} from '../response'; // TODO add handleQueryCollection() diff --git a/packages/core/src/servers/http/index.ts b/packages/servers/http/src/index.ts similarity index 100% rename from packages/core/src/servers/http/index.ts rename to packages/servers/http/src/index.ts diff --git a/packages/core/src/servers/http/response.ts b/packages/servers/http/src/response.ts similarity index 89% rename from packages/core/src/servers/http/response.ts rename to packages/servers/http/src/response.ts index f38218d..1c64c7f 100644 --- a/packages/core/src/servers/http/response.ts +++ b/packages/servers/http/src/response.ts @@ -1,5 +1,5 @@ -import {Language, LanguageStatusMessageMap} from '../../common'; -import {MiddlewareResponseError, Response} from '../../backend/common'; +import {Language, LanguageStatusMessageMap} from '@modal-sh/yasumi'; +import {MiddlewareResponseError, Response} from '@modal-sh/yasumi/backend'; interface PlainResponseParams extends Response { body?: T; diff --git a/packages/core/src/servers/http/utils.ts b/packages/servers/http/src/utils.ts similarity index 94% rename from packages/core/src/servers/http/utils.ts rename to packages/servers/http/src/utils.ts index 6443284..c204504 100644 --- a/packages/core/src/servers/http/utils.ts +++ b/packages/servers/http/src/utils.ts @@ -1,5 +1,5 @@ import {IncomingMessage} from 'http'; -import {PATCH_CONTENT_TYPES} from '../../common'; +import {PATCH_CONTENT_TYPES} from '@modal-sh/yasumi'; export const isTextMediaType = (mediaType: string) => ( mediaType.startsWith('text/') diff --git a/packages/core/test/features/decorators.test.ts b/packages/servers/http/test/features/decorators.test.ts similarity index 92% rename from packages/core/test/features/decorators.test.ts rename to packages/servers/http/test/features/decorators.test.ts index e93b9ac..688055d 100644 --- a/packages/core/test/features/decorators.test.ts +++ b/packages/servers/http/test/features/decorators.test.ts @@ -1,8 +1,8 @@ import {describe, afterAll, beforeAll, it} from 'vitest'; -import {Application, application, resource, Resource, validation as v} from '../../src/common'; -import {Backend, DataSource, RequestContext} from '../../src/backend'; +import {Application, application, resource, Resource, validation as v} from '@modal-sh/yasumi'; +import {Backend, DataSource, RequestContext} from '@modal-sh/yasumi/backend'; +import {httpExtender, HttpServer} from '../../src'; import {createTestClient, DummyDataSource, dummyGenerationStrategy, TEST_LANGUAGE, TestClient} from '../utils'; -import {httpExtender, HttpServer} from '../../src/servers/http'; const PORT = 3001; const HOST = '127.0.0.1'; diff --git a/packages/core/test/handlers/http/default.test.ts b/packages/servers/http/test/handlers/default.test.ts similarity index 98% rename from packages/core/test/handlers/http/default.test.ts rename to packages/servers/http/test/handlers/default.test.ts index f147071..ee98c42 100644 --- a/packages/core/test/handlers/http/default.test.ts +++ b/packages/servers/http/test/handlers/default.test.ts @@ -9,16 +9,16 @@ import { vi, } from 'vitest'; import {constants} from 'http2'; -import {Backend, DataSource} from '../../../src/backend'; +import {Backend, DataSource} from '@modal-sh/yasumi/backend'; import { application, resource, validation as v, Resource, Application, -} from '../../../src/common'; -import {createTestClient, DummyDataSource, dummyGenerationStrategy, TEST_LANGUAGE, TestClient} from '../../utils'; -import {httpExtender, HttpServer} from '../../../src/servers/http'; +} from '@modal-sh/yasumi'; +import {createTestClient, DummyDataSource, dummyGenerationStrategy, TEST_LANGUAGE, TestClient} from '../utils'; +import {httpExtender, HttpServer} from '../../src'; const PORT = 3000; const HOST = '127.0.0.1'; diff --git a/packages/core/test/handlers/http/error-handling.test.ts b/packages/servers/http/test/handlers/error-handling.test.ts similarity index 98% rename from packages/core/test/handlers/http/error-handling.test.ts rename to packages/servers/http/test/handlers/error-handling.test.ts index 56c3808..017bbbf 100644 --- a/packages/core/test/handlers/http/error-handling.test.ts +++ b/packages/servers/http/test/handlers/error-handling.test.ts @@ -9,8 +9,8 @@ import { vi, } from 'vitest'; import {constants} from 'http2'; -import {Backend, DataSource} from '../../../src/backend'; -import {application, resource, validation as v, Resource, Application, Delta} from '../../../src/common'; +import {Backend, DataSource} from '@modal-sh/yasumi/backend'; +import {application, resource, validation as v, Resource, Application, Delta} from '@modal-sh/yasumi'; import { createTestClient, TestClient, @@ -18,8 +18,8 @@ import { DummyError, TEST_LANGUAGE, dummyGenerationStrategy, -} from '../../utils'; -import {httpExtender, HttpServer} from '../../../src/servers/http'; +} from '../utils'; +import {httpExtender, HttpServer} from '../../src'; const PORT = 3001; const HOST = '127.0.0.1'; diff --git a/packages/servers/http/test/utils.ts b/packages/servers/http/test/utils.ts new file mode 100644 index 0000000..b1a5f0a --- /dev/null +++ b/packages/servers/http/test/utils.ts @@ -0,0 +1,480 @@ +import {IncomingHttpHeaders, IncomingMessage, OutgoingHttpHeaders, request, RequestOptions} from 'http'; +import {Method, DataSource} from '@modal-sh/yasumi/backend'; +import {FALLBACK_LANGUAGE, Language} from '@modal-sh/yasumi'; + +interface ClientParams { + method: Method; + path: string; + headers?: IncomingHttpHeaders; + body?: unknown; +} + +type ResponseBody = Buffer | string | object; + +export interface TestClient { + (params: ClientParams): Promise<[IncomingMessage, ResponseBody?]>; + acceptMediaType(mediaType: string): this; + acceptLanguage(language: string): this; + acceptCharset(charset: string): this; + contentType(mediaType: string): this; + contentCharset(charset: string): this; +} + +export const createTestClient = (options: Omit): TestClient => { + const additionalHeaders: OutgoingHttpHeaders = {}; + const client = (params: ClientParams) => new Promise<[IncomingMessage, ResponseBody?]>((resolve, reject) => { + const { + ...etcAdditionalHeaders + } = additionalHeaders; + + // odd that request() uses OutgoingHttpHeaders instead of IncomingHttpHeaders... + const headers: OutgoingHttpHeaders = { + ...(options.headers ?? {}), + ...etcAdditionalHeaders, + ...(params.headers ?? {}), + }; + + let contentTypeHeader: string | undefined; + if (typeof params.body !== 'undefined') { + contentTypeHeader = headers['content-type'] = params.headers?.['content-type'] ?? 'application/json'; + } + + const req = request({ + ...options, + method: params.method, + path: params.path, + headers, + }); + + req.on('response', (res) => { + // if (req.method.toUpperCase() === 'QUERY') { + // res.statusMessage = ''; + // res.statusCode = 200; + // } + + res.on('error', (err) => { + reject(err); + }); + + let resBuffer: Buffer | undefined; + res.on('data', (c) => { + resBuffer = ( + typeof resBuffer === 'undefined' + ? Buffer.from(c) + : Buffer.concat([resBuffer, c]) + ); + }); + + res.on('close', () => { + const acceptHeader = Array.isArray(headers['accept']) ? headers['accept'].join('; ') : headers['accept']; + const contentTypeBase = acceptHeader ?? 'application/octet-stream'; + const [type, subtype] = contentTypeBase.split('/'); + const allSubtypes = subtype.split('+'); + if (typeof resBuffer !== 'undefined') { + if (allSubtypes.includes('json')) { + const acceptCharset = ( + Array.isArray(headers['accept-charset']) + ? headers['accept-charset'].join('; ') + : headers['accept-charset'] + ) as BufferEncoding | undefined; + resolve([res, JSON.parse(resBuffer.toString(acceptCharset ?? 'utf-8'))]); + return; + } + + if (type === 'text') { + const acceptCharset = ( + Array.isArray(headers['accept-charset']) + ? headers['accept-charset'].join('; ') + : headers['accept-charset'] + ) as BufferEncoding | undefined; + resolve([res, resBuffer.toString(acceptCharset ?? 'utf-8')]); + return; + } + + resolve([res, resBuffer]); + return; + } + + resolve([res]); + }); + }); + + req.on('error', (err) => { + reject(err); + }) + + if (typeof params.body !== 'undefined') { + const theContentTypeHeader = Array.isArray(contentTypeHeader) ? contentTypeHeader.join('; ') : contentTypeHeader?.toString(); + const contentTypeAll = theContentTypeHeader ?? 'application/octet-stream'; + const [contentTypeBase, ...contentTypeParams] = contentTypeAll.split(';').map((s) => s.replace(/\s+/g, '').trim()); + const charsetParam = contentTypeParams.find((s) => s.startsWith('charset=')); + const charset = charsetParam?.split('=')?.[1] as BufferEncoding | undefined; + const [, subtype] = contentTypeBase.split('/'); + const allSubtypes = subtype.split('+'); + req.write( + allSubtypes.includes('json') + ? JSON.stringify(params.body) + : Buffer.from(params.body?.toString() ?? '', contentTypeBase === 'text' ? charset : undefined) + ); + } + + req.end(); + }); + + client.acceptMediaType = function acceptMediaType(mediaType: string) { + additionalHeaders['accept'] = mediaType; + return this; + }; + + client.acceptLanguage = function acceptLanguage(language: string) { + additionalHeaders['accept-language'] = language; + return this; + }; + + client.acceptCharset = function acceptCharset(charset: string) { + additionalHeaders['accept-charset'] = charset; + return this; + }; + + client.contentType = function contentType(mediaType: string) { + additionalHeaders['content-type'] = mediaType; + return this; + }; + + client.contentCharset = function contentCharset(charset: string) { + additionalHeaders['content-type'] = `${additionalHeaders['content-type']}; charset="${charset}"`; + return this; + }; + + return client; +}; + +export const dummyGenerationStrategy = () => Promise.resolve(); + +export class DummyError extends Error {} + +export class DummyDataSource implements DataSource { + private resource?: { dataSource?: unknown }; + + async create(): Promise { + return {}; + } + + async delete(): Promise {} + + async emplace(): Promise<[object, boolean]> { + return [{}, false]; + } + + async getById(): Promise { + return {}; + } + + async newId(): Promise { + return ''; + } + + async getMultiple(): Promise { + return []; + } + + async getSingle(): Promise { + return {}; + } + + async getTotalCount(): Promise { + return 0; + } + + async initialize(): Promise {} + + async patch(): Promise { + return {}; + } + + prepareResource(rr: unknown) { + this.resource = rr as unknown as { dataSource: DummyDataSource }; + this.resource.dataSource = this; + } +} + +export const TEST_LANGUAGE: Language = { + name: FALLBACK_LANGUAGE.name, + statusMessages: { + resourceCollectionQueried: '$Resource Collection Queried', + unableToSerializeResponse: 'Unable To Serialize Response', + unableToEncodeResponse: 'Unable To Encode Response', + unableToBindResourceDataSource: 'Unable To Bind $RESOURCE Data Source', + unableToInitializeResourceDataSource: 'Unable To Initialize $RESOURCE Data Source', + unableToFetchResourceCollection: 'Unable To Fetch $RESOURCE Collection', + unableToFetchResource: 'Unable To Fetch $RESOURCE', + unableToDeleteResource: 'Unable To Delete $RESOURCE', + languageNotAcceptable: 'Language Not Acceptable', + characterSetNotAcceptable: 'Character Set Not Acceptable', + unableToDeserializeResource: 'Unable To Deserialize $RESOURCE', + unableToDecodeResource: 'Unable To Decode $RESOURCE', + mediaTypeNotAcceptable: 'Media Type Not Acceptable', + methodNotAllowed: 'Method Not Allowed', + urlNotFound: 'URL Not Found', + badRequest: 'Bad Request', + ok: 'OK', + provideOptions: 'Provide Options', + resourceCollectionFetched: '$RESOURCE Collection Fetched', + resourceFetched: '$RESOURCE Fetched', + resourceNotFound: '$RESOURCE Not Found', + deleteNonExistingResource: 'Delete Non-Existing $RESOURCE', + resourceDeleted: '$RESOURCE Deleted', + unableToDeserializeRequest: 'Unable To Deserialize Request', + patchNonExistingResource: 'Patch Non-Existing $RESOURCE', + unableToPatchResource: 'Unable To Patch $RESOURCE', + invalidResourcePatch: 'Invalid $RESOURCE Patch', + invalidResourcePatchType: 'Invalid $RESOURCE Patch Type', + invalidResource: 'Invalid $RESOURCE', + resourcePatched: '$RESOURCE Patched', + resourceCreated: '$RESOURCE Created', + resourceReplaced: '$RESOURCE Replaced', + unableToGenerateIdFromResourceDataSource: 'Unable To Generate ID From $RESOURCE Data Source', + unableToAssignIdFromResourceDataSource: 'Unable To Assign ID From $RESOURCE Data Source', + unableToEmplaceResource: 'Unable To Emplace $RESOURCE', + resourceIdNotGiven: '$RESOURCE ID Not Given', + unableToCreateResource: 'Unable To Create $RESOURCE', + notImplemented: 'Not Implemented', + internalServerError: 'Internal Server Error', + }, + bodies: { + badRequest: [ + 'An invalid request has been made.', + [ + 'Check if the request body has all the required attributes for this endpoint.', + 'Check if the request body has only the valid attributes for this endpoint.', + 'Check if the request body matches the schema for the resource associated with this endpoint.', + 'Check if the request is appropriate for this endpoint.', + ], + ], + languageNotAcceptable: [ + 'The server could not process a response suitable for the client\'s provided language requirement.', + [ + 'Choose from the available languages on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + characterSetNotAcceptable: [ + 'The server could not process a response suitable for the client\'s provided character set requirement.', + [ + 'Choose from the available character sets on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + mediaTypeNotAcceptable: [ + 'The server could not process a response suitable for the client\'s provided media type requirement.', + [ + 'Choose from the available media types on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + deleteNonExistingResource: [ + 'The client has attempted to delete a resource that does not exist.', + [ + 'Ensure that the resource still exists.', + 'Ensure that the correct method is provided.', + ], + ], + internalServerError: [ + 'An unknown error has occurred within the service.', + [ + 'Try the request again at a later time.', + 'Contact the administrator if the service remains in a degraded or non-functional state.', + ], + ], + invalidResource: [ + 'The request has an invalid structure or is missing some attributes.', + [ + 'Check if the request body has all the required attributes for this endpoint.', + 'Check if the request body has only the valid attributes for this endpoint.', + 'Check if the request body matches the schema for the resource associated with this endpoint.', + ], + ], + invalidResourcePatch: [ + 'The request has an invalid patch data.', + [ + 'Check if the appropriate patch type is specified on the request data.', + 'Check if the request body has all the required attributes for this endpoint.', + 'Check if the request body has only the valid attributes for this endpoint.', + 'Check if the request body matches the schema for the resource associated with this endpoint.', + ], + ], + invalidResourcePatchType: [ + 'The request has an invalid or unsupported kind of patch data.', + [ + 'Check if the appropriate patch type is specified on the request data.', + 'Check if the request body has all the required attributes for this endpoint.', + 'Check if the request body has only the valid attributes for this endpoint.', + 'Check if the request body matches the schema for the resource associated with this endpoint.', + ], + ], + methodNotAllowed: [ + 'A request with an invalid or unsupported method has been made.', + [ + 'Check if the request method is appropriate for this endpoint.', + 'Check if the client is authorized to perform the method on this endpoint.', + ] + ], + notImplemented: [ + 'The service does not have any implementation for the accessed endpoint.', + [ + 'Try the request again at a later time.', + 'Contact the administrator if the service remains in a degraded or non-functional state.', + ], + ], + patchNonExistingResource: [ + 'The client has attempted to patch a resource that does not exist.', + [ + 'Ensure that the resource still exists.', + 'Ensure that the correct method is provided.', + ], + ], + resourceIdNotGiven: [ + 'The resource ID is not provided for the accessed endpoint.', + [ + 'Check if the resource ID is provided and valid in the URL.', + 'Check if the request method is appropriate for this endpoint.', + ], + ], + unableToAssignIdFromResourceDataSource: [ + 'The resource could not be assigned an ID from the associated data source.', + [ + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToBindResourceDataSource: [ + 'The resource could not be associated from the data source.', + [ + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToCreateResource: [ + 'An error has occurred on creating the resource.', + [ + 'Check if the request method is appropriate for this endpoint.', + 'Check if the request body has all the required attributes for this endpoint.', + 'Check if the request body has only the valid attributes for this endpoint.', + 'Check if the request body matches the schema for the resource associated with this endpoint.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToDecodeResource: [ + 'The resource byte array could not be decoded for the provided character set.', + [ + 'Choose from the available character sets on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + unableToDeleteResource: [ + 'An error has occurred on deleting the resource.', + [ + 'Check if the request method is appropriate for this endpoint.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToDeserializeRequest: [ + 'The decoded request byte array could not be deserialized for the provided media type.', + [ + 'Choose from the available media types on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + unableToDeserializeResource: [ + 'The decoded resource could not be deserialized for the provided media type.', + [ + 'Choose from the available media types on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + unableToEmplaceResource: [ + 'An error has occurred on emplacing the resource.', + [ + 'Check if the request method is appropriate for this endpoint.', + 'Check if the request body has all the required attributes for this endpoint.', + 'Check if the request body has only the valid attributes for this endpoint.', + 'Check if the request body matches the schema for the resource associated with this endpoint.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToEncodeResponse: [ + 'The response data could not be encoded for the provided character set.', + [ + 'Choose from the available character sets on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + unableToFetchResource: [ + 'An error has occurred on fetching the resource.', + [ + 'Check if the request method is appropriate for this endpoint.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToFetchResourceCollection: [ + 'An error has occurred on fetching the resource collection.', + [ + 'Check if the request method is appropriate for this endpoint.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToGenerateIdFromResourceDataSource: [ + 'The associated data source for the resource could not produce an ID.', + [ + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToInitializeResourceDataSource: [ + 'The associated data source for the resource could not be connected for usage.', + [ + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToPatchResource: [ + 'An error has occurred on patching the resource.', + [ + 'Check if the request method is appropriate for this endpoint.', + 'Check if the request body has all the required attributes for this endpoint.', + 'Check if the request body has only the valid attributes for this endpoint.', + 'Check if the request body matches the schema for the resource associated with this endpoint.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + unableToSerializeResponse: [ + 'The response data could not be serialized for the provided media type.', + [ + 'Choose from the available media types on this service.', + 'Contact the administrator to provide localization for the client\'s given requirements.', + ], + ], + urlNotFound: [ + 'An endpoint in the provided URL could not be found.', + [ + 'Check if the request URL is correct.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + resourceNotFound: [ + 'The resource in the provided URL could not be found.', + [ + 'Check if the request URL is correct.', + 'Try the request again at a later time.', + 'Contact the administrator regarding missing configuration or unavailability of dependencies.', + ], + ], + }, +}; diff --git a/packages/servers/http/tsconfig.json b/packages/servers/http/tsconfig.json new file mode 100644 index 0000000..74083d7 --- /dev/null +++ b/packages/servers/http/tsconfig.json @@ -0,0 +1,23 @@ +{ + "exclude": ["node_modules"], + "include": ["src", "types"], + "compilerOptions": { + "module": "ESNext", + "lib": ["ESNext"], + "importHelpers": true, + "declaration": true, + "sourceMap": true, + "rootDir": "./src", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "bundler", + "jsx": "react", + "esModuleInterop": true, + "target": "es2018", + "useDefineForClassFields": false, + "declarationMap": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 167139c..b5fd8a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: packages/core: dependencies: - negotiator: - specifier: ^0.6.3 - version: 0.6.3 tsx: specifier: ^4.7.1 version: 4.7.2 @@ -18,9 +15,6 @@ importers: specifier: ^0.30.0 version: 0.30.0 devDependencies: - '@types/negotiator': - specifier: ^0.6.3 - version: 0.6.3 '@types/node': specifier: ^20.11.30 version: 20.12.7 @@ -40,7 +34,7 @@ importers: packages/data-sources/duckdb: dependencies: '@modal-sh/yasumi': - specifier: '*' + specifier: workspace:* version: link:../../core duckdb-async: specifier: ^0.10.0 @@ -65,7 +59,7 @@ importers: packages/data-sources/file-jsonl: dependencies: '@modal-sh/yasumi': - specifier: '*' + specifier: workspace:* version: link:../../core devDependencies: '@types/node': @@ -87,11 +81,14 @@ importers: packages/examples/cms-web-api: dependencies: '@modal-sh/yasumi': - specifier: '*' + specifier: workspace:* version: link:../../core '@modal-sh/yasumi-data-source-file-jsonl': - specifier: '*' + specifier: workspace:* version: link:../../data-sources/file-jsonl + '@modal-sh/yasumi-server-http': + specifier: workspace:* + version: link:../../servers/http tsx: specifier: ^4.7.1 version: 4.7.2 @@ -115,11 +112,14 @@ importers: packages/examples/duckdb: dependencies: '@modal-sh/yasumi': - specifier: '*' + specifier: workspace:* version: link:../../core '@modal-sh/yasumi-data-source-duckdb': - specifier: '*' + specifier: workspace:* version: link:../../data-sources/duckdb + '@modal-sh/yasumi-server-http': + specifier: workspace:* + version: link:../../servers/http tsx: specifier: ^4.7.1 version: 4.7.2 @@ -140,6 +140,34 @@ importers: specifier: ^1.2.0 version: 1.5.0(@types/node@20.12.7) + packages/servers/http: + dependencies: + '@modal-sh/yasumi': + specifier: workspace:* + version: link:../../core + negotiator: + specifier: ^0.6.3 + version: 0.6.3 + devDependencies: + '@types/negotiator': + specifier: ^0.6.3 + version: 0.6.3 + '@types/node': + specifier: ^20.11.0 + version: 20.12.7 + pridepack: + specifier: 2.6.0 + version: 2.6.0(tslib@2.6.2)(typescript@5.4.5) + tslib: + specifier: ^2.6.2 + version: 2.6.2 + typescript: + specifier: ^5.3.3 + version: 5.4.5 + vitest: + specifier: ^1.2.0 + version: 1.4.0(@types/node@20.12.7) + packages: /@esbuild/aix-ppc64@0.19.12: @@ -739,6 +767,14 @@ packages: undici-types: 5.26.5 dev: true + /@vitest/expect@1.4.0: + resolution: {integrity: sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==} + dependencies: + '@vitest/spy': 1.4.0 + '@vitest/utils': 1.4.0 + chai: 4.4.1 + dev: true + /@vitest/expect@1.5.0: resolution: {integrity: sha512-0pzuCI6KYi2SIC3LQezmxujU9RK/vwC1U9R0rLuGlNGcOuDWxqWKu6nUdFsX9tH1WU0SXtAxToOsEjeUn1s3hA==} dependencies: @@ -747,6 +783,14 @@ packages: chai: 4.4.1 dev: true + /@vitest/runner@1.4.0: + resolution: {integrity: sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==} + dependencies: + '@vitest/utils': 1.4.0 + p-limit: 5.0.0 + pathe: 1.1.2 + dev: true + /@vitest/runner@1.5.0: resolution: {integrity: sha512-7HWwdxXP5yDoe7DTpbif9l6ZmDwCzcSIK38kTSIt6CFEpMjX4EpCgT6wUmS0xTXqMI6E/ONmfgRKmaujpabjZQ==} dependencies: @@ -755,6 +799,14 @@ packages: pathe: 1.1.2 dev: true + /@vitest/snapshot@1.4.0: + resolution: {integrity: sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==} + dependencies: + magic-string: 0.30.9 + pathe: 1.1.2 + pretty-format: 29.7.0 + dev: true + /@vitest/snapshot@1.5.0: resolution: {integrity: sha512-qpv3fSEuNrhAO3FpH6YYRdaECnnRjg9VxbhdtPwPRnzSfHVXnNzzrpX4cJxqiwgRMo7uRMWDFBlsBq4Cr+rO3A==} dependencies: @@ -763,12 +815,27 @@ packages: pretty-format: 29.7.0 dev: true + /@vitest/spy@1.4.0: + resolution: {integrity: sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==} + dependencies: + tinyspy: 2.2.1 + dev: true + /@vitest/spy@1.5.0: resolution: {integrity: sha512-vu6vi6ew5N5MMHJjD5PoakMRKYdmIrNJmyfkhRpQt5d9Ewhw9nZ5Aqynbi3N61bvk9UvZ5UysMT6ayIrZ8GA9w==} dependencies: tinyspy: 2.2.1 dev: true + /@vitest/utils@1.4.0: + resolution: {integrity: sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==} + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + dev: true + /@vitest/utils@1.5.0: resolution: {integrity: sha512-BDU0GNL8MWkRkSRdNFvCUCAVOeHaUlVJ9Tx0TYBZyXaaOTmGtUFObzchCivIBrIwKzvZA7A9sCejVhXM2aY98A==} dependencies: @@ -2305,6 +2372,27 @@ packages: resolution: {integrity: sha512-5POBdbSkM+3nvJ6ZlyQHsggisfRtyT4tVTo1EIIShs6qCdXJnyWU5TJ68vr8iTg5zpOLjXLRiBqNx+9zwZz/rA==} dev: false + /vite-node@1.4.0(@types/node@20.12.7): + resolution: {integrity: sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + pathe: 1.1.2 + picocolors: 1.0.0 + vite: 5.2.8(@types/node@20.12.7) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vite-node@1.5.0(@types/node@20.12.7): resolution: {integrity: sha512-tV8h6gMj6vPzVCa7l+VGq9lwoJjW8Y79vst8QZZGiuRAfijU+EEWuc0kFpmndQrWhMMhet1jdSF+40KSZUqIIw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2362,6 +2450,62 @@ packages: fsevents: 2.3.3 dev: true + /vitest@1.4.0(@types/node@20.12.7): + resolution: {integrity: sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.4.0 + '@vitest/ui': 1.4.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 20.12.7 + '@vitest/expect': 1.4.0 + '@vitest/runner': 1.4.0 + '@vitest/snapshot': 1.4.0 + '@vitest/spy': 1.4.0 + '@vitest/utils': 1.4.0 + acorn-walk: 8.3.2 + chai: 4.4.1 + debug: 4.3.4 + execa: 8.0.1 + local-pkg: 0.5.0 + magic-string: 0.30.9 + pathe: 1.1.2 + picocolors: 1.0.0 + std-env: 3.7.0 + strip-literal: 2.1.0 + tinybench: 2.7.0 + tinypool: 0.8.4 + vite: 5.2.8(@types/node@20.12.7) + vite-node: 1.4.0(@types/node@20.12.7) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vitest@1.5.0(@types/node@20.12.7): resolution: {integrity: sha512-d8UKgR0m2kjdxDWX6911uwxout6GHS0XaGH1cksSIVVG8kRlE7G7aBw7myKQCvDI5dT4j7ZMa+l706BIORMDLw==} engines: {node: ^18.0.0 || >=20.0.0}