From a688840e4c1a65bfde0c0a0a2e6bcd2e42880b51 Mon Sep 17 00:00:00 2001 From: Dhemy Date: Sun, 12 Jul 2026 22:13:59 +0200 Subject: [PATCH 01/17] wip: add architecture overview for KoalaTs v3 Update README.md --- docs/architecture/README.md | 62 +++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/architecture/README.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..f6cb942 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,62 @@ +# KoalaTs Architecture + +> [!NOTE] +> This document describes the architecture of KoalaTs starting from the `v3` which is a work in progress and a +> complete rewrite of the framework. + +## Overview + +KoalaTs is a monorepo made of three package types: + +- "Component": A standalone reusable library. It solves one focused problem and can be used without KoalaTs. +- "Bridge": An integration layer between KoalaTs components and an external library or ecosystem. +- "Bundle": The application-level composition of components and bridges. + +## Goals + +KoalaTs is a modern, easy-to-use, functional-programming-first framework. It separates application behavior from +external runtime integrations. + +## Architecture + +### Framework Bundle + +The framework bundle is the composition root of an application. It combines components and bridges with an application +manifest. + +```typescript +koala(bridges)(manifest); +``` + +## Application composition + +```mermaid +flowchart TD + Components["Components"] + Bridges["Bridges"] + Manifest["Application manifest"] + Bundle["Framework bundle"] + App["Koala application"] + Components --> Bundle + Bridges --> Bundle + Manifest --> Bundle + Bundle --> App +``` + +The bundle assembles an application from its manifest and the selected components and bridges. + +## Application behavior + +An application can expose multiple entry points, such as HTTP, console, or MCP. Each entry point is supported by a +component and connected to its runtime environment through a bridge. + +```mermaid +flowchart LR + EntryPoints["Application entry points"] + Components["Components"] + Bridges["Bridges"] + Runtime["External runtime environments"] + EntryPoints --> Components + Components --> Bridges + Bridges --> Runtime +``` From 4d0d6b159574a687c757ce541d07c8d4b8bbca1f Mon Sep 17 00:00:00 2001 From: Dhemy Date: Sun, 12 Jul 2026 22:21:03 +0200 Subject: [PATCH 02/17] wip: add bridge examples --- docs/architecture/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index f6cb942..7a8c91f 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -45,6 +45,13 @@ flowchart TD The bundle assembles an application from its manifest and the selected components and bridges. +### Bridge examples + +| Bridge | Connects | +|---------------------|----------------------------------| +| Fastify bridge | HTTP component ↔ Fastify | +| Node console bridge | Console component ↔ Node process | + ## Application behavior An application can expose multiple entry points, such as HTTP, console, or MCP. Each entry point is supported by a From 845af503c466649f714c1c76cc9ff1c7f7716396 Mon Sep 17 00:00:00 2001 From: Dhemy Date: Mon, 13 Jul 2026 22:51:06 +0200 Subject: [PATCH 03/17] feat(contracts): add event dispatcher contracts --- src/packages/contracts/event-dispatcher.ts | 27 ++++++++++++++++++++++ src/packages/contracts/index.ts | 2 +- src/packages/contracts/package.json | 6 +++++ src/packages/contracts/tsconfig.json | 3 +++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/packages/contracts/event-dispatcher.ts diff --git a/src/packages/contracts/event-dispatcher.ts b/src/packages/contracts/event-dispatcher.ts new file mode 100644 index 0000000..cfbc1d0 --- /dev/null +++ b/src/packages/contracts/event-dispatcher.ts @@ -0,0 +1,27 @@ +/** + * An event is a message produced by an emitter. + * + * @typeParam Name - The event name. + * @typeParam Payload - The payload carried by the event. + */ +export type Event = { + /** Identifies the event type. */ + name: Name; + /** Carries the event-specific data. */ + payload: Payload; +}; + +/** + * A callback that receives an {@link Event}. + * It may be synchronous or asynchronous. + * + * @typeParam E - The event received by the listener. + */ +export type EventListener = (event: E) => void | Promise; + +/** + * Dispatches an event to its listeners. + * + * @typeParam E - The event this emitter can dispatch. + */ +export type EventEmitter = (event: E) => Promise; diff --git a/src/packages/contracts/index.ts b/src/packages/contracts/index.ts index cb0ff5c..d5fad27 100644 --- a/src/packages/contracts/index.ts +++ b/src/packages/contracts/index.ts @@ -1 +1 @@ -export {}; +export type { Event, EventEmitter, EventListener } from '#contracts/event-dispatcher'; diff --git a/src/packages/contracts/package.json b/src/packages/contracts/package.json index b2263fd..f2c4451 100644 --- a/src/packages/contracts/package.json +++ b/src/packages/contracts/package.json @@ -15,6 +15,12 @@ "import": "./dist/index.js" } }, + "imports": { + "#contracts/*": { + "types": "./*.ts", + "default": "./dist/*.js" + } + }, "files": [ "dist" ], diff --git a/src/packages/contracts/tsconfig.json b/src/packages/contracts/tsconfig.json index ef6f9f9..9b1d69b 100644 --- a/src/packages/contracts/tsconfig.json +++ b/src/packages/contracts/tsconfig.json @@ -1,6 +1,9 @@ { "extends": "../../../tsconfig.json", "compilerOptions": { + "paths": { + "#contracts/*": ["./*.ts"] + }, "rootDir": ".", "outDir": "./dist" }, From 5769c30225d0a876d6687d9f51f4563752cd8677 Mon Sep 17 00:00:00 2001 From: Dhemy Date: Fri, 17 Jul 2026 23:06:41 +0200 Subject: [PATCH 04/17] chore: reorder generics and keep event identity --- src/packages/contracts/event-dispatcher.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/packages/contracts/event-dispatcher.ts b/src/packages/contracts/event-dispatcher.ts index cfbc1d0..5fafcbb 100644 --- a/src/packages/contracts/event-dispatcher.ts +++ b/src/packages/contracts/event-dispatcher.ts @@ -1,10 +1,10 @@ /** * An event is a message produced by an emitter. * - * @typeParam Name - The event name. * @typeParam Payload - The payload carried by the event. + * @typeParam Name - The event name. */ -export type Event = { +export type Event = { /** Identifies the event type. */ name: Name; /** Carries the event-specific data. */ From 82202c2d1bd894170e0a222d2c43554e84f60d8b Mon Sep 17 00:00:00 2001 From: Dhemy Date: Sat, 18 Jul 2026 09:58:23 +0200 Subject: [PATCH 05/17] wip: move types to the event dispatcher component --- .../event-dispatcher.ts | 0 src/packages/event-dispatcher/index.ts | 1 + src/packages/event-dispatcher/package.json | 46 +++++++++++++++++++ src/packages/event-dispatcher/tsconfig.json | 12 +++++ 4 files changed, 59 insertions(+) rename src/packages/{contracts => event-dispatcher}/event-dispatcher.ts (100%) create mode 100644 src/packages/event-dispatcher/index.ts create mode 100644 src/packages/event-dispatcher/package.json create mode 100644 src/packages/event-dispatcher/tsconfig.json diff --git a/src/packages/contracts/event-dispatcher.ts b/src/packages/event-dispatcher/event-dispatcher.ts similarity index 100% rename from src/packages/contracts/event-dispatcher.ts rename to src/packages/event-dispatcher/event-dispatcher.ts diff --git a/src/packages/event-dispatcher/index.ts b/src/packages/event-dispatcher/index.ts new file mode 100644 index 0000000..78adbf4 --- /dev/null +++ b/src/packages/event-dispatcher/index.ts @@ -0,0 +1 @@ +export type { Event, EventEmitter, EventListener } from '#event-dispatcher/event-dispatcher'; diff --git a/src/packages/event-dispatcher/package.json b/src/packages/event-dispatcher/package.json new file mode 100644 index 0000000..a4283da --- /dev/null +++ b/src/packages/event-dispatcher/package.json @@ -0,0 +1,46 @@ +{ + "name": "@koala-ts/event-dispatcher", + "version": "2.0.0", + "description": "Event dispatcher component for KoalaTs", + "repository": { + "type": "git", + "url": "git+https://github.com/koala-ts/framework.git" + }, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "imports": { + "#event-dispatcher/*": { + "types": "./*.ts", + "default": "./dist/*.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "publint": "publint .", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "author": { + "name": "Dhemy", + "email": "imdhemy@gmail.com", + "url": "https://imdhemy.com" + }, + "license": "Apache-2.0", + "engines": { + "node": ">=24", + "npm": ">=11" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/packages/event-dispatcher/tsconfig.json b/src/packages/event-dispatcher/tsconfig.json new file mode 100644 index 0000000..93507db --- /dev/null +++ b/src/packages/event-dispatcher/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "paths": { + "#event-dispatcher/*": ["./*.ts"] + }, + "rootDir": ".", + "outDir": "./dist" + }, + "include": ["./*.ts"], + "exclude": ["./*.test.ts", "./dist"] +} From bbb3fe1a73fcd2b671d14b6017973889c5881dfb Mon Sep 17 00:00:00 2001 From: Dhemy Date: Sat, 18 Jul 2026 11:02:11 +0200 Subject: [PATCH 06/17] delete unnecessary component --- .../event-dispatcher/event-dispatcher.ts | 27 ----------- src/packages/event-dispatcher/index.ts | 1 - src/packages/event-dispatcher/package.json | 46 ------------------- src/packages/event-dispatcher/tsconfig.json | 12 ----- 4 files changed, 86 deletions(-) delete mode 100644 src/packages/event-dispatcher/event-dispatcher.ts delete mode 100644 src/packages/event-dispatcher/index.ts delete mode 100644 src/packages/event-dispatcher/package.json delete mode 100644 src/packages/event-dispatcher/tsconfig.json diff --git a/src/packages/event-dispatcher/event-dispatcher.ts b/src/packages/event-dispatcher/event-dispatcher.ts deleted file mode 100644 index 5fafcbb..0000000 --- a/src/packages/event-dispatcher/event-dispatcher.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * An event is a message produced by an emitter. - * - * @typeParam Payload - The payload carried by the event. - * @typeParam Name - The event name. - */ -export type Event = { - /** Identifies the event type. */ - name: Name; - /** Carries the event-specific data. */ - payload: Payload; -}; - -/** - * A callback that receives an {@link Event}. - * It may be synchronous or asynchronous. - * - * @typeParam E - The event received by the listener. - */ -export type EventListener = (event: E) => void | Promise; - -/** - * Dispatches an event to its listeners. - * - * @typeParam E - The event this emitter can dispatch. - */ -export type EventEmitter = (event: E) => Promise; diff --git a/src/packages/event-dispatcher/index.ts b/src/packages/event-dispatcher/index.ts deleted file mode 100644 index 78adbf4..0000000 --- a/src/packages/event-dispatcher/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { Event, EventEmitter, EventListener } from '#event-dispatcher/event-dispatcher'; diff --git a/src/packages/event-dispatcher/package.json b/src/packages/event-dispatcher/package.json deleted file mode 100644 index a4283da..0000000 --- a/src/packages/event-dispatcher/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "@koala-ts/event-dispatcher", - "version": "2.0.0", - "description": "Event dispatcher component for KoalaTs", - "repository": { - "type": "git", - "url": "git+https://github.com/koala-ts/framework.git" - }, - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "imports": { - "#event-dispatcher/*": { - "types": "./*.ts", - "default": "./dist/*.js" - } - }, - "files": [ - "dist" - ], - "scripts": { - "build": "tsc -p tsconfig.json", - "publint": "publint .", - "typecheck": "tsc -p tsconfig.json --noEmit" - }, - "author": { - "name": "Dhemy", - "email": "imdhemy@gmail.com", - "url": "https://imdhemy.com" - }, - "license": "Apache-2.0", - "engines": { - "node": ">=24", - "npm": ">=11" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - } -} diff --git a/src/packages/event-dispatcher/tsconfig.json b/src/packages/event-dispatcher/tsconfig.json deleted file mode 100644 index 93507db..0000000 --- a/src/packages/event-dispatcher/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "compilerOptions": { - "paths": { - "#event-dispatcher/*": ["./*.ts"] - }, - "rootDir": ".", - "outDir": "./dist" - }, - "include": ["./*.ts"], - "exclude": ["./*.test.ts", "./dist"] -} From 517c3f3b4b50f39837d267bbb576b4b11f7b7bb2 Mon Sep 17 00:00:00 2001 From: Dhemy Date: Sat, 18 Jul 2026 22:49:23 +0200 Subject: [PATCH 07/17] feat(http): handle raw request --- src/packages/contracts/index.ts | 1 - src/packages/http/foundation/http-request.ts | 12 +++++ src/packages/http/foundation/http-response.ts | 26 +++++++++++ src/packages/http/index.ts | 4 ++ .../http/kernel/handle-raw-request.test.ts | 28 +++++++++++ .../http/kernel/handle-raw-request.ts | 36 +++++++++++++++ src/packages/http/package.json | 46 +++++++++++++++++++ src/packages/http/tsconfig.json | 12 +++++ tests/request-props.e2e.test.ts | 2 +- vitest.config.ts | 27 +++++++++++ 10 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 src/packages/http/foundation/http-request.ts create mode 100644 src/packages/http/foundation/http-response.ts create mode 100644 src/packages/http/index.ts create mode 100644 src/packages/http/kernel/handle-raw-request.test.ts create mode 100644 src/packages/http/kernel/handle-raw-request.ts create mode 100644 src/packages/http/package.json create mode 100644 src/packages/http/tsconfig.json diff --git a/src/packages/contracts/index.ts b/src/packages/contracts/index.ts index d5fad27..e69de29 100644 --- a/src/packages/contracts/index.ts +++ b/src/packages/contracts/index.ts @@ -1 +0,0 @@ -export type { Event, EventEmitter, EventListener } from '#contracts/event-dispatcher'; diff --git a/src/packages/http/foundation/http-request.ts b/src/packages/http/foundation/http-request.ts new file mode 100644 index 0000000..5b20827 --- /dev/null +++ b/src/packages/http/foundation/http-request.ts @@ -0,0 +1,12 @@ +/** Values captured from a matched HTTP route path. */ +export type HttpRouteParams = Readonly>; + +/** + * An HTTP request supplied to a controller. + */ +export type HttpRequest = Readonly<{ + /** The complete standard Web request. */ + message: Request; + /** Values captured from the route path. */ + params: HttpRouteParams; +}>; diff --git a/src/packages/http/foundation/http-response.ts b/src/packages/http/foundation/http-response.ts new file mode 100644 index 0000000..7af8561 --- /dev/null +++ b/src/packages/http/foundation/http-response.ts @@ -0,0 +1,26 @@ +/** + * A declarative HTTP header value. + */ +export type HttpHeaderValue = string | readonly string[]; + +/** + * Declarative HTTP response headers. + * Header names are normalized to lowercase by HTTP bridges. + */ +export type HttpHeaders = Readonly>; + +/** + * A declarative HTTP response returned by a controller. + * + * @typeParam Body - The response body interpreted by the selected HTTP bridge. + */ +export type HttpResponse = Readonly<{ + /** The HTTP status code. */ + status: number; + /** The optional HTTP reason phrase. */ + statusText?: string; + /** Declarative response headers. */ + headers: HttpHeaders; + /** The response body. */ + body: Body; +}>; diff --git a/src/packages/http/index.ts b/src/packages/http/index.ts new file mode 100644 index 0000000..0efa46f --- /dev/null +++ b/src/packages/http/index.ts @@ -0,0 +1,4 @@ +export type { HttpRequest, HttpRouteParams } from '#http/foundation/http-request'; +export type { HttpHeaders, HttpHeaderValue, HttpResponse } from '#http/foundation/http-response'; +export { handleRawRequest } from '#http/kernel/handle-raw-request'; +export type { Controller, RawRequest, RequestContext } from '#http/kernel/handle-raw-request'; diff --git a/src/packages/http/kernel/handle-raw-request.test.ts b/src/packages/http/kernel/handle-raw-request.test.ts new file mode 100644 index 0000000..b4ec71b --- /dev/null +++ b/src/packages/http/kernel/handle-raw-request.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it, vi } from 'vitest'; +import type { HttpResponse } from '#http/foundation/http-response'; +import { type Controller, handleRawRequest, type RawRequest } from '#http/kernel/handle-raw-request'; + +describe('Handle raw request', () => { + it('invokes the controller with http request', async () => { + const response: HttpResponse = { + status: 200, + statusText: 'OK', + headers: {}, + body: 'Hello, world!', + }; + + const rawRequest: RawRequest = { + message: new Request('https://koala.test.user/123'), + context: { params: { userId: '123' } }, + controller: vi.fn().mockResolvedValue(response), + }; + + const result = await handleRawRequest(rawRequest); + + expect(rawRequest.controller).toHaveBeenCalledWith({ + message: rawRequest.message, + params: rawRequest.context.params, + }); + expect(result).toEqual(response); + }); +}); diff --git a/src/packages/http/kernel/handle-raw-request.ts b/src/packages/http/kernel/handle-raw-request.ts new file mode 100644 index 0000000..9ade6da --- /dev/null +++ b/src/packages/http/kernel/handle-raw-request.ts @@ -0,0 +1,36 @@ +import type { HttpRequest, HttpRouteParams } from '#http/foundation/http-request'; +import type { HttpResponse } from '#http/foundation/http-response'; + +/** + * Handles an HTTP request and returns a declarative HTTP response. + */ +export type Controller = (request: HttpRequest) => HttpResponse | Promise; + +/** Information required to handle a request. */ +export type RequestContext = Readonly<{ + /** Values captured from the matched route path. */ + params: HttpRouteParams; +}>; + +/** + * The kernel input used to invoke a controller. + * + * It combines the standard Web request, route-matching context, and the + * controller that receives the resulting {@link HttpRequest}. + */ +export type RawRequest = Readonly<{ + /** The complete standard Web request. */ + message: Request; + /** Framework information associated with the request. */ + context: RequestContext; + /** The controller invoked with the prepared HTTP request. */ + controller: Controller; +}>; + +type HandleRawRequest = (rawRequest: RawRequest) => Promise; + +export const handleRawRequest: HandleRawRequest = async ({ controller, message, context }) => + controller({ + message, + params: context.params, + }); diff --git a/src/packages/http/package.json b/src/packages/http/package.json new file mode 100644 index 0000000..59d72dc --- /dev/null +++ b/src/packages/http/package.json @@ -0,0 +1,46 @@ +{ + "name": "@koala-ts/http", + "version": "2.0.0", + "description": "HTTP component for KoalaTs", + "repository": { + "type": "git", + "url": "git+https://github.com/koala-ts/framework.git" + }, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "imports": { + "#http/*": { + "types": "./*.ts", + "default": "./dist/*.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "publint": "publint .", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "author": { + "name": "Dhemy", + "email": "imdhemy@gmail.com", + "url": "https://imdhemy.com" + }, + "license": "Apache-2.0", + "engines": { + "node": ">=24", + "npm": ">=11" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/src/packages/http/tsconfig.json b/src/packages/http/tsconfig.json new file mode 100644 index 0000000..c86d9cc --- /dev/null +++ b/src/packages/http/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "paths": { + "#http/*": ["./*.ts"] + }, + "rootDir": ".", + "outDir": "./dist" + }, + "include": ["./*.ts"], + "exclude": ["./*.test.ts", "./dist"] +} diff --git a/tests/request-props.e2e.test.ts b/tests/request-props.e2e.test.ts index b95f5b0..b6cdb9c 100644 --- a/tests/request-props.e2e.test.ts +++ b/tests/request-props.e2e.test.ts @@ -1,6 +1,6 @@ import { text } from 'node:stream/consumers'; import { describe, expect, test } from 'vitest'; -import { createTestAgent, type HttpRequest, type HttpScope, Route, type UploadedFile } from '../src'; +import { createTestAgent, type HttpRequest, type HttpScope, Route, type UploadedFile } from '../src/index.js'; interface MyRequest extends HttpRequest { body: { name: string }; diff --git a/vitest.config.ts b/vitest.config.ts index fe18571..03e8977 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,10 +1,37 @@ +import { existsSync, readdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; import { configDefaults, defineConfig } from 'vitest/config'; +type PackageManifest = { + imports?: Record; +}; + +const packagesDirectory = path.resolve(__dirname, './src/packages'); + +const packageAliases = Object.fromEntries( + readdirSync(packagesDirectory, { withFileTypes: true }) + .filter(entry => entry.isDirectory()) + .flatMap(entry => { + const packageDirectory = path.join(packagesDirectory, entry.name); + const manifestPath = path.join(packageDirectory, 'package.json'); + + if (!existsSync(manifestPath)) { + return []; + } + + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as PackageManifest; + + return Object.keys(manifest.imports ?? {}) + .filter(specifier => specifier.endsWith('/*')) + .map(specifier => [specifier.slice(0, -2), packageDirectory] as const); + }), +); + export default defineConfig({ resolve: { alias: { '#koala': path.resolve(__dirname, './src'), + ...packageAliases, }, }, test: { From 3b2bade1344e2e7bd62d410c3b172811d8f08d5c Mon Sep 17 00:00:00 2001 From: Dhemy Date: Sat, 18 Jul 2026 23:44:43 +0200 Subject: [PATCH 08/17] chore(http): introduce raw http message type --- src/packages/http/foundation/http-request.ts | 31 +++++++++++++++++-- src/packages/http/foundation/http-response.ts | 13 +++++--- src/packages/http/index.ts | 6 ++-- .../http/kernel/handle-raw-request.test.ts | 9 +++++- .../http/kernel/handle-raw-request.ts | 10 +++--- 5 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/packages/http/foundation/http-request.ts b/src/packages/http/foundation/http-request.ts index 5b20827..7dfe73b 100644 --- a/src/packages/http/foundation/http-request.ts +++ b/src/packages/http/foundation/http-request.ts @@ -1,12 +1,37 @@ /** Values captured from a matched HTTP route path. */ -export type HttpRouteParams = Readonly>; +type HttpRouteParams = Readonly>; + +/** A received HTTP request header value. */ +type HttpRequestHeaderValue = string | readonly string[]; + +/** HTTP request headers supplied by a bridge. */ +type HttpRequestHeaders = { + /** A received header value indexed by its normalized lowercase name. */ + readonly [name: string]: HttpRequestHeaderValue; +}; + +/** + * A raw HTTP message received by Koala. + * + * It contains no eagerly parsed query, cookies, or body. + */ +export type HttpRequestMessage = Readonly<{ + /** The HTTP method as received. */ + method: string; + /** The raw HTTP request target as a URI reference, usually `/path?query`. */ + url: string; + /** The HTTP headers supplied by the bridge. */ + headers: HttpRequestHeaders; + /** A single-consumption source of raw body bytes. */ + body: AsyncIterable; +}>; /** * An HTTP request supplied to a controller. */ export type HttpRequest = Readonly<{ - /** The complete standard Web request. */ - message: Request; + /** The raw HTTP message. */ + message: HttpRequestMessage; /** Values captured from the route path. */ params: HttpRouteParams; }>; diff --git a/src/packages/http/foundation/http-response.ts b/src/packages/http/foundation/http-response.ts index 7af8561..e7023ee 100644 --- a/src/packages/http/foundation/http-response.ts +++ b/src/packages/http/foundation/http-response.ts @@ -1,13 +1,16 @@ /** - * A declarative HTTP header value. + * A declarative HTTP response header value. */ -export type HttpHeaderValue = string | readonly string[]; +type HttpResponseHeaderValue = string | readonly string[]; /** * Declarative HTTP response headers. * Header names are normalized to lowercase by HTTP bridges. */ -export type HttpHeaders = Readonly>; +type HttpResponseHeaders = { + /** A response header value indexed by its normalized lowercase name. */ + readonly [name: string]: HttpResponseHeaderValue; +}; /** * A declarative HTTP response returned by a controller. @@ -19,8 +22,8 @@ export type HttpResponse = Readonly<{ status: number; /** The optional HTTP reason phrase. */ statusText?: string; - /** Declarative response headers. */ - headers: HttpHeaders; + /** Response headers. */ + headers: HttpResponseHeaders; /** The response body. */ body: Body; }>; diff --git a/src/packages/http/index.ts b/src/packages/http/index.ts index 0efa46f..a52b2bf 100644 --- a/src/packages/http/index.ts +++ b/src/packages/http/index.ts @@ -1,4 +1,4 @@ -export type { HttpRequest, HttpRouteParams } from '#http/foundation/http-request'; -export type { HttpHeaders, HttpHeaderValue, HttpResponse } from '#http/foundation/http-response'; -export { handleRawRequest } from '#http/kernel/handle-raw-request'; +export type { HttpRequest, HttpRequestMessage } from '#http/foundation/http-request'; +export type { HttpResponse } from '#http/foundation/http-response'; export type { Controller, RawRequest, RequestContext } from '#http/kernel/handle-raw-request'; +export { handleRawRequest } from '#http/kernel/handle-raw-request'; diff --git a/src/packages/http/kernel/handle-raw-request.test.ts b/src/packages/http/kernel/handle-raw-request.test.ts index b4ec71b..19ea504 100644 --- a/src/packages/http/kernel/handle-raw-request.test.ts +++ b/src/packages/http/kernel/handle-raw-request.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; +import type { HttpRequestMessage } from '#http/foundation/http-request'; import type { HttpResponse } from '#http/foundation/http-response'; import { type Controller, handleRawRequest, type RawRequest } from '#http/kernel/handle-raw-request'; @@ -10,9 +11,15 @@ describe('Handle raw request', () => { headers: {}, body: 'Hello, world!', }; + const message: HttpRequestMessage = { + method: 'GET', + url: '/user/123', + headers: {}, + body: (async function* () {})(), + }; const rawRequest: RawRequest = { - message: new Request('https://koala.test.user/123'), + message, context: { params: { userId: '123' } }, controller: vi.fn().mockResolvedValue(response), }; diff --git a/src/packages/http/kernel/handle-raw-request.ts b/src/packages/http/kernel/handle-raw-request.ts index 9ade6da..4385f9a 100644 --- a/src/packages/http/kernel/handle-raw-request.ts +++ b/src/packages/http/kernel/handle-raw-request.ts @@ -1,4 +1,4 @@ -import type { HttpRequest, HttpRouteParams } from '#http/foundation/http-request'; +import type { HttpRequest, HttpRequestMessage } from '#http/foundation/http-request'; import type { HttpResponse } from '#http/foundation/http-response'; /** @@ -9,18 +9,18 @@ export type Controller = (request: HttpRequest) => HttpResponse | Promise; /** * The kernel input used to invoke a controller. * - * It combines the standard Web request, route-matching context, and the + * It combines the raw HTTP message, route-matching context, and the * controller that receives the resulting {@link HttpRequest}. */ export type RawRequest = Readonly<{ - /** The complete standard Web request. */ - message: Request; + /** The raw HTTP message. */ + message: HttpRequestMessage; /** Framework information associated with the request. */ context: RequestContext; /** The controller invoked with the prepared HTTP request. */ From 8080feff4c4e8404a17327bbff8169907b7feac5 Mon Sep 17 00:00:00 2001 From: Dhemy Date: Sun, 19 Jul 2026 22:31:02 +0200 Subject: [PATCH 09/17] chore: improve make file --- makefile | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/makefile b/makefile index 1a526fb..51fa8ac 100644 --- a/makefile +++ b/makefile @@ -1,16 +1,23 @@ IMAGE_NAME ?= koalats-framework +CONTAINER_NAME ?= $(IMAGE_NAME)-container +HOST_PORT ?= 3000 -.PHONY: build bash start rebuild +.PHONY: build bash start stop -start: build bash +start: + @if docker ps --format '{{.Names}}' | grep -qx $(CONTAINER_NAME); then \ + echo "$(CONTAINER_NAME) is already running"; \ + else \ + docker run --rm --detach --name $(CONTAINER_NAME) --publish $(HOST_PORT):3000 --volume $(CURDIR):/app $(IMAGE_NAME) tail -f /dev/null; \ + fi build: - @if ! docker images | grep -q $(IMAGE_NAME); then \ - docker build -t $(IMAGE_NAME) . ; \ - fi + docker build --no-cache -t $(IMAGE_NAME) . bash: - docker run --rm -it --name $(IMAGE_NAME)-container -v ${PWD}:/app $(IMAGE_NAME) /bin/sh + docker exec --interactive --tty $(CONTAINER_NAME) /bin/sh -rebuild: - docker build --no-cache -t $(IMAGE_NAME) . +stop: + @if docker container inspect $(CONTAINER_NAME) >/dev/null 2>&1; then \ + docker rm --force $(CONTAINER_NAME); \ + fi From a12f07fcd7b3aac1aa34780bab9ac2d3650b272e Mon Sep 17 00:00:00 2001 From: Dhemy Date: Wed, 22 Jul 2026 20:18:47 +0200 Subject: [PATCH 10/17] add HTTP bridge contract --- src/packages/contracts/http-bridge.test.ts | 102 +++++++++++++++++++++ src/packages/contracts/http-bridge.ts | 89 ++++++++++++++++++ src/packages/contracts/index.ts | 1 + src/packages/contracts/package.json | 6 +- vitest.config.ts | 29 +++++- 5 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 src/packages/contracts/http-bridge.test.ts create mode 100644 src/packages/contracts/http-bridge.ts diff --git a/src/packages/contracts/http-bridge.test.ts b/src/packages/contracts/http-bridge.test.ts new file mode 100644 index 0000000..ca350f7 --- /dev/null +++ b/src/packages/contracts/http-bridge.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it, test } from 'vitest'; +import { + httpLifecycleFailure, + httpLifecycleStartFailed, + httpLifecycleStopFailed, + httpLifecycleSuccess, + isHttpLifecycleFailure, + isHttpLifecycleSuccess, +} from '#contracts/http-bridge'; + +describe('HTTP bridge', () => { + describe('HTTP lifecycle', () => { + test('start failed factory', () => { + const cause = new Error('unavailable'); + + const actual = httpLifecycleStartFailed(cause); + + expect(actual).toEqual({ + code: 'START_FAILED', + message: 'Failed to start HTTP server', + cause, + }); + }); + + test('stop failed factory', () => { + const cause = new Error('unavailable'); + + const actual = httpLifecycleStopFailed(cause); + + expect(actual).toEqual({ + code: 'STOP_FAILED', + message: 'Failed to stop HTTP server', + cause, + }); + }); + + test('success result factory', () => { + const value = { address: 'http://127.0.0.1:3000' }; + + const actual = httpLifecycleSuccess(value); + + expect(actual).toEqual({ _tag: 'Success', value }); + }); + + it('failed result factory', () => { + const error = { + code: 'START_FAILED', + message: 'unavailable', + } as const; + + const actual = httpLifecycleFailure(error); + + expect(actual).toEqual({ _tag: 'Failure', error }); + }); + + test.each([ + [ + 'success checker recognizes a successful lifecycle result as successful', + () => httpLifecycleSuccess(undefined), + true, + ], + [ + 'success checker does not recognize a failed lifecycle result as successful', + () => + httpLifecycleFailure({ + code: 'START_FAILED', + message: 'unavailable', + }), + false, + ], + ])('%s', (_, createResult, expected) => { + const result = createResult(); + + const actual = isHttpLifecycleSuccess(result); + + expect(actual).toBe(expected); + }); + + test.each([ + [ + 'failure checker does not recognize a successful lifecycle result as failed', + () => httpLifecycleSuccess(undefined), + false, + ], + [ + 'failure checker recognizes a failed lifecycle result as failed', + () => + httpLifecycleFailure({ + code: 'STOP_FAILED', + message: 'unavailable', + }), + true, + ], + ])('%s', (_, createResult, expected) => { + const result = createResult(); + + const actual = isHttpLifecycleFailure(result); + + expect(actual).toBe(expected); + }); + }); +}); diff --git a/src/packages/contracts/http-bridge.ts b/src/packages/contracts/http-bridge.ts new file mode 100644 index 0000000..e2b5d97 --- /dev/null +++ b/src/packages/contracts/http-bridge.ts @@ -0,0 +1,89 @@ +/** + * A failure reported by an HTTP bridge lifecycle operation. + */ +export type HttpLifecycleError = Readonly<{ + readonly code: 'START_FAILED' | 'STOP_FAILED'; + readonly message: string; + readonly cause?: unknown; +}>; + +/** Creates an error for a failed HTTP server start. */ +type HttpLifecycleStartFailureFactory = (cause?: unknown) => HttpLifecycleError; + +export const httpLifecycleStartFailed: HttpLifecycleStartFailureFactory = cause => ({ + code: 'START_FAILED', + message: 'Failed to start HTTP server', + cause, +}); + +/** Creates an error for a failed HTTP server stop. */ +type HttpLifecycleStopFailureFactory = (cause?: unknown) => HttpLifecycleError; + +export const httpLifecycleStopFailed: HttpLifecycleStopFailureFactory = cause => ({ + code: 'STOP_FAILED', + message: 'Failed to stop HTTP server', + cause, +}); + +/** + * A successful HTTP bridge lifecycle operation. + */ +type HttpLifecycleSuccess = Readonly<{ + readonly _tag: 'Success'; + readonly value: Value; +}>; + +/** + * A failed HTTP bridge lifecycle operation. + */ +type HttpLifecycleFailure = Readonly<{ + readonly _tag: 'Failure'; + readonly error: HttpLifecycleError; +}>; + +/** + * The outcome of an HTTP bridge lifecycle operation. + */ +export type HttpLifecycleResult = HttpLifecycleSuccess | HttpLifecycleFailure; + +/** Creates a successful HTTP bridge lifecycle result. */ +type HttpLifecycleSuccessFactory = (value: Value) => HttpLifecycleSuccess; + +export const httpLifecycleSuccess: HttpLifecycleSuccessFactory = value => ({ + _tag: 'Success', + value, +}); + +/** Creates a failed HTTP bridge lifecycle result. */ +type HttpLifecycleFailureFactory = (error: HttpLifecycleError) => HttpLifecycleFailure; + +export const httpLifecycleFailure: HttpLifecycleFailureFactory = error => ({ + _tag: 'Failure', + error, +}); + +/** Determines whether an HTTP bridge lifecycle result is successful. */ +type IsHttpLifecycleSuccess = (result: HttpLifecycleResult) => result is HttpLifecycleSuccess; + +export const isHttpLifecycleSuccess: IsHttpLifecycleSuccess = result => result._tag === 'Success'; + +/** Determines whether an HTTP bridge lifecycle result is a failure. */ +type IsHttpLifecycleFailure = (result: HttpLifecycleResult) => result is HttpLifecycleFailure; + +export const isHttpLifecycleFailure: IsHttpLifecycleFailure = result => result._tag === 'Failure'; + +/** + * Network binding configuration passed to an HTTP bridge. + */ +export type HttpListenOptions = Readonly<{ + readonly port: number; + readonly host?: string; +}>; + +/** + * Runtime capability implemented by an HTTP server integration. + */ +export type HttpBridge = Readonly<{ + readonly start: (listen: HttpListenOptions) => Promise>; + readonly stop: () => Promise>; +}>; diff --git a/src/packages/contracts/index.ts b/src/packages/contracts/index.ts index e69de29..cb0ff5c 100644 --- a/src/packages/contracts/index.ts +++ b/src/packages/contracts/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/src/packages/contracts/package.json b/src/packages/contracts/package.json index f2c4451..2fab6bc 100644 --- a/src/packages/contracts/package.json +++ b/src/packages/contracts/package.json @@ -10,9 +10,9 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "./http-bridge": { + "types": "./dist/http-bridge.d.ts", + "import": "./dist/http-bridge.js" } }, "imports": { diff --git a/vitest.config.ts b/vitest.config.ts index 03e8977..98c839c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,9 +3,21 @@ import path from 'node:path'; import { configDefaults, defineConfig } from 'vitest/config'; type PackageManifest = { + name?: string; + exports?: Record; imports?: Record; }; +type PackageExport = Readonly<{ + types: string; +}>; + +const isPackageExport = (value: unknown): value is PackageExport => + typeof value === 'object' && value !== null && 'types' in value && typeof value.types === 'string'; + +const sourcePathFromTypesPath = (typesPath: string): string => + typesPath.replace('./dist/', './').replace('.d.ts', '.ts'); + const packagesDirectory = path.resolve(__dirname, './src/packages'); const packageAliases = Object.fromEntries( @@ -21,9 +33,24 @@ const packageAliases = Object.fromEntries( const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as PackageManifest; - return Object.keys(manifest.imports ?? {}) + const internalAliases = Object.keys(manifest.imports ?? {}) .filter(specifier => specifier.endsWith('/*')) .map(specifier => [specifier.slice(0, -2), packageDirectory] as const); + + const publicAliases = Object.entries(manifest.exports ?? {}) + .filter( + (entry): entry is [string, PackageExport] => + manifest.name !== undefined && entry[0].startsWith('./') && isPackageExport(entry[1]), + ) + .map( + ([specifier, target]) => + [ + `${manifest.name}/${specifier.slice(2)}`, + path.resolve(packageDirectory, sourcePathFromTypesPath(target.types)), + ] as const, + ); + + return [...internalAliases, ...publicAliases]; }), ); From df237aec8c18056b3a2f234db8456619bb3f6908 Mon Sep 17 00:00:00 2001 From: Dhemy Date: Wed, 22 Jul 2026 20:18:53 +0200 Subject: [PATCH 11/17] add HTTP application lifecycle --- .../framework-bundle/http/lifecycle.test.ts | 87 +++++++++++++++++++ .../framework-bundle/http/lifecycle.ts | 29 +++++++ src/packages/framework-bundle/index.ts | 1 + src/packages/framework-bundle/koala.test.ts | 20 +++++ src/packages/framework-bundle/koala.ts | 31 +++++++ src/packages/framework-bundle/package.json | 49 +++++++++++ src/packages/framework-bundle/tsconfig.json | 12 +++ 7 files changed, 229 insertions(+) create mode 100644 src/packages/framework-bundle/http/lifecycle.test.ts create mode 100644 src/packages/framework-bundle/http/lifecycle.ts create mode 100644 src/packages/framework-bundle/index.ts create mode 100644 src/packages/framework-bundle/koala.test.ts create mode 100644 src/packages/framework-bundle/koala.ts create mode 100644 src/packages/framework-bundle/package.json create mode 100644 src/packages/framework-bundle/tsconfig.json diff --git a/src/packages/framework-bundle/http/lifecycle.test.ts b/src/packages/framework-bundle/http/lifecycle.test.ts new file mode 100644 index 0000000..82b89bf --- /dev/null +++ b/src/packages/framework-bundle/http/lifecycle.test.ts @@ -0,0 +1,87 @@ +import type { HttpBridge } from '@koala-ts/contracts/http-bridge'; +import { describe, expect, it } from 'vitest'; +import { start, stop } from '#framework-bundle/http/lifecycle'; +import { koala } from '#framework-bundle/koala'; + +describe('http lifecycle', () => { + it('starts an app through its bridge', async () => { + const manifest = { type: 'http', listen: { port: 3000 } } as const; + let receivedListen: unknown; + const bridge: HttpBridge = { + start: async listen => { + receivedListen = listen; + return { _tag: 'Success', value: undefined }; + }, + stop: async () => ({ _tag: 'Success', value: undefined }), + }; + const app = koala(manifest, bridge); + + const server = await start(app); + + expect(receivedListen).toBe(app.manifest.listen); + expect(server).toEqual({ + _tag: 'Success', + value: { app }, + }); + }); + + it('returns a bridge failure when it cannot start an app', async () => { + const manifest = { type: 'http', listen: { port: 3000 } } as const; + const failure = { + _tag: 'Failure', + error: { + code: 'START_FAILED', + message: 'unavailable', + }, + } as const; + const bridge: HttpBridge = { + start: async () => failure, + stop: async () => ({ _tag: 'Success', value: undefined }), + }; + const app = koala(manifest, bridge); + + const actual = await start(app); + + expect(actual).toBe(failure); + }); + + it('returns its app when stopped', async () => { + const manifest = { type: 'http', listen: { port: 3000 } } as const; + const bridgeStopCalls: boolean[] = []; + const bridge: HttpBridge = { + start: async () => ({ _tag: 'Success', value: undefined }), + stop: async () => { + bridgeStopCalls.push(true); + return { _tag: 'Success', value: undefined }; + }, + }; + const app = koala(manifest, bridge); + const server = { app }; + + const actual = await stop(server); + + expect(bridgeStopCalls).toEqual([true]); + expect(actual).toEqual({ _tag: 'Success', value: app }); + }); + + it('returns a bridge failure when it cannot stop an app', async () => { + const manifest = { type: 'http', listen: { port: 3000 } } as const; + const failure = { + _tag: 'Failure', + error: { + code: 'STOP_FAILED', + message: 'unavailable', + }, + } as const; + const bridge: HttpBridge = { + start: async () => ({ _tag: 'Success', value: undefined }), + stop: async () => failure, + }; + const app = koala(manifest, bridge); + const server = { app }; + + const actual = await stop(server); + + expect(actual).toBe(failure); + }); +}); diff --git a/src/packages/framework-bundle/http/lifecycle.ts b/src/packages/framework-bundle/http/lifecycle.ts new file mode 100644 index 0000000..056b38d --- /dev/null +++ b/src/packages/framework-bundle/http/lifecycle.ts @@ -0,0 +1,29 @@ +import { + type HttpLifecycleResult, + httpLifecycleSuccess, + isHttpLifecycleFailure, +} from '@koala-ts/contracts/http-bridge'; +import type { KoalaApp } from '#framework-bundle/koala'; + +/** + * A running HTTP application and its cleanup capability. + */ +export type HttpServer = Readonly<{ + readonly app: KoalaApp; +}>; + +/** Starts a Koala application through its bridge. */ +export type Start = (app: KoalaApp) => Promise>; +export const start: Start = async app => { + const result = await app.bridge.start(app.manifest.listen); + + return isHttpLifecycleFailure(result) ? result : httpLifecycleSuccess({ app }); +}; + +/** Stops a running Koala application. */ +export type Stop = (server: HttpServer) => Promise>; +export const stop: Stop = async server => { + const result = await server.app.bridge.stop(); + + return isHttpLifecycleFailure(result) ? result : httpLifecycleSuccess(server.app); +}; diff --git a/src/packages/framework-bundle/index.ts b/src/packages/framework-bundle/index.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/src/packages/framework-bundle/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/src/packages/framework-bundle/koala.test.ts b/src/packages/framework-bundle/koala.test.ts new file mode 100644 index 0000000..16c94af --- /dev/null +++ b/src/packages/framework-bundle/koala.test.ts @@ -0,0 +1,20 @@ +import type { HttpBridge } from '@koala-ts/contracts/http-bridge'; +import { describe, expect, it } from 'vitest'; +import { koala } from '#framework-bundle/koala'; + +describe('koala', () => { + it('creates a koala application', () => { + const manifest = { + type: 'http', + listen: { port: 3000 }, + } as const; + const bridge: HttpBridge = { + start: async () => ({ _tag: 'Success', value: undefined }), + stop: async () => ({ _tag: 'Success', value: undefined }), + }; + + const app = koala(manifest, bridge); + + expect(app).toEqual({ manifest, bridge }); + }); +}); diff --git a/src/packages/framework-bundle/koala.ts b/src/packages/framework-bundle/koala.ts new file mode 100644 index 0000000..4363045 --- /dev/null +++ b/src/packages/framework-bundle/koala.ts @@ -0,0 +1,31 @@ +import type { HttpBridge, HttpListenOptions } from '@koala-ts/contracts/http-bridge'; + +/** + * Immutable configuration for an HTTP application. + */ +export type HttpManifest = Readonly<{ + readonly type: 'http'; + readonly listen: HttpListenOptions; +}>; + +/** + * The Koala application type. + */ +export type KoalaApp = Readonly<{ + readonly manifest: HttpManifest; + readonly bridge: HttpBridge; +}>; + +/** + * Creates a Koala application. + * + * @example + * ```ts + * const app = koala( + * { type: 'http', listen: { port: 3000 } }, + * fastify(), + * ); + * ``` + */ +type Koala = (manifest: HttpManifest, bridge: HttpBridge) => KoalaApp; +export const koala: Koala = (manifest, bridge) => ({ manifest, bridge }); diff --git a/src/packages/framework-bundle/package.json b/src/packages/framework-bundle/package.json new file mode 100644 index 0000000..7e04346 --- /dev/null +++ b/src/packages/framework-bundle/package.json @@ -0,0 +1,49 @@ +{ + "name": "@koala-ts/framework-bundle", + "version": "2.0.0", + "description": "Framework bundle for KoalaTs", + "repository": { + "type": "git", + "url": "git+https://github.com/koala-ts/framework.git" + }, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "imports": { + "#framework-bundle/*": { + "types": "./*.ts", + "default": "./dist/*.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "publint": "publint .", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "author": { + "name": "Dhemy", + "email": "imdhemy@gmail.com", + "url": "https://imdhemy.com" + }, + "license": "Apache-2.0", + "engines": { + "node": ">=24", + "npm": ">=11" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "dependencies": { + "@koala-ts/contracts": "*" + } +} diff --git a/src/packages/framework-bundle/tsconfig.json b/src/packages/framework-bundle/tsconfig.json new file mode 100644 index 0000000..687e390 --- /dev/null +++ b/src/packages/framework-bundle/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "paths": { + "#framework-bundle/*": ["./*.ts"] + }, + "rootDir": ".", + "outDir": "./dist" + }, + "include": ["./**/*.ts"], + "exclude": ["./**/*.test.ts", "./dist"] +} From 0bdab9613bf2ba41076b6db863f8ae28ea8e821e Mon Sep 17 00:00:00 2001 From: Dhemy Date: Wed, 22 Jul 2026 20:18:58 +0200 Subject: [PATCH 12/17] add Fastify HTTP bridge --- .../.github/workflows/publish.yml | 30 ++++++++++ src/packages/fastify-bridge/fastify.test.ts | 49 +++++++++++++++++ src/packages/fastify-bridge/fastify.ts | 37 +++++++++++++ src/packages/fastify-bridge/index.ts | 14 +++++ src/packages/fastify-bridge/package.json | 55 +++++++++++++++++++ src/packages/fastify-bridge/tsconfig.json | 12 ++++ 6 files changed, 197 insertions(+) create mode 100644 src/packages/fastify-bridge/.github/workflows/publish.yml create mode 100644 src/packages/fastify-bridge/fastify.test.ts create mode 100644 src/packages/fastify-bridge/fastify.ts create mode 100644 src/packages/fastify-bridge/index.ts create mode 100644 src/packages/fastify-bridge/package.json create mode 100644 src/packages/fastify-bridge/tsconfig.json diff --git a/src/packages/fastify-bridge/.github/workflows/publish.yml b/src/packages/fastify-bridge/.github/workflows/publish.yml new file mode 100644 index 0000000..a93fae5 --- /dev/null +++ b/src/packages/fastify-bridge/.github/workflows/publish.yml @@ -0,0 +1,30 @@ +name: Publish + +on: + push: + tags: + - '*' + workflow_dispatch: + inputs: + release_tag: + description: Component tag that provides the package version + required: true + type: string + dry_run: + description: Validate the package without publishing it + required: true + default: true + type: boolean + +permissions: + contents: read + id-token: write + +jobs: + publish: + uses: koala-ts/framework/.github/workflows/publish-component.yml@2.x + with: + framework_ref: 2.x + release_tag: ${{ inputs.release_tag || github.ref_name }} + dry_run: ${{ inputs.dry_run || false }} + secrets: inherit diff --git a/src/packages/fastify-bridge/fastify.test.ts b/src/packages/fastify-bridge/fastify.test.ts new file mode 100644 index 0000000..c11208a --- /dev/null +++ b/src/packages/fastify-bridge/fastify.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createFastifyBridge } from '#fastify-bridge/fastify'; + +describe('Fastify bridge', () => { + describe('bridge start', () => { + it('starts the runtime using the requested listen options', async () => { + const listen = { port: 3000, host: '127.0.0.1' } as const; + const runtime = { + listen: vi.fn(), + close: vi.fn(), + }; + const bridge = createFastifyBridge(() => runtime); + + const actual = await bridge.start(listen); + + expect(runtime.listen).toHaveBeenCalledWith(listen); + expect(actual).toEqual({ _tag: 'Success', value: undefined }); + }); + }); + + describe('bridge stop', () => { + it('stops a started runtime', async () => { + const runtime = { + listen: vi.fn(), + close: vi.fn(), + }; + const bridge = createFastifyBridge(() => runtime); + await bridge.start({ port: 3000 }); + + const actual = await bridge.stop(); + + expect(runtime.close).toHaveBeenCalledOnce(); + expect(actual).toEqual({ _tag: 'Success', value: undefined }); + }); + + it('completes before the runtime starts', async () => { + const runtime = { + listen: vi.fn(), + close: vi.fn(), + }; + const bridge = createFastifyBridge(() => runtime); + + const actual = await bridge.stop(); + + expect(runtime.close).not.toHaveBeenCalled(); + expect(actual).toEqual({ _tag: 'Success', value: undefined }); + }); + }); +}); diff --git a/src/packages/fastify-bridge/fastify.ts b/src/packages/fastify-bridge/fastify.ts new file mode 100644 index 0000000..ce1ff22 --- /dev/null +++ b/src/packages/fastify-bridge/fastify.ts @@ -0,0 +1,37 @@ +import { type HttpBridge, type HttpListenOptions, httpLifecycleSuccess } from '@koala-ts/contracts/http-bridge'; + +/** + * The Fastify runtime capabilities required by the bridge. + */ +type FastifyRuntime = Readonly<{ + readonly listen: (options: HttpListenOptions) => Promise; + readonly close: () => Promise; +}>; + +/** + * Creates a fresh Fastify runtime for each bridge start cycle. + */ +type FastifyRuntimeFactory = () => FastifyRuntime; + +/** + * Creates an HTTP bridge from a Fastify runtime factory. + */ +type CreateFastifyBridge = (createRuntime: FastifyRuntimeFactory) => HttpBridge; + +export const createFastifyBridge: CreateFastifyBridge = createRuntime => { + let runtime: FastifyRuntime | undefined; + + return { + start: async listen => { + runtime = createRuntime(); + await runtime.listen(listen); + + return httpLifecycleSuccess(undefined); + }, + stop: async () => { + await runtime?.close(); + + return httpLifecycleSuccess(undefined); + }, + }; +}; diff --git a/src/packages/fastify-bridge/index.ts b/src/packages/fastify-bridge/index.ts new file mode 100644 index 0000000..a9397f7 --- /dev/null +++ b/src/packages/fastify-bridge/index.ts @@ -0,0 +1,14 @@ +import type { HttpBridge } from '@koala-ts/contracts/http-bridge'; +import createFastifyRuntime, { type FastifyServerOptions } from 'fastify'; +import { createFastifyBridge } from '#fastify-bridge/fastify'; + +/** + * Creates an HTTP bridge backed by Fastify. + * + * Fastify options remain local to this bridge; consumers interact with the + * returned value through the framework bundle's runtime-neutral contract. + */ +type Fastify = (options?: FastifyServerOptions) => HttpBridge; + +export const fastify: Fastify = options => + createFastifyBridge(() => (options === undefined ? createFastifyRuntime() : createFastifyRuntime(options))); diff --git a/src/packages/fastify-bridge/package.json b/src/packages/fastify-bridge/package.json new file mode 100644 index 0000000..ee12921 --- /dev/null +++ b/src/packages/fastify-bridge/package.json @@ -0,0 +1,55 @@ +{ + "name": "@koala-ts/fastify-bridge", + "version": "2.0.0", + "description": "Fastify bridge for KoalaTs", + "repository": { + "type": "git", + "url": "git+https://github.com/koala-ts/framework.git" + }, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "imports": { + "#fastify-bridge/*": { + "types": "./*.ts", + "default": "./dist/*.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "publint": "publint .", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "author": { + "name": "Dhemy", + "email": "imdhemy@gmail.com", + "url": "https://imdhemy.com" + }, + "license": "Apache-2.0", + "engines": { + "node": ">=24", + "npm": ">=11" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "dependencies": { + "@koala-ts/contracts": "*" + }, + "peerDependencies": { + "fastify": "^5" + }, + "devDependencies": { + "fastify": "^5" + } +} diff --git a/src/packages/fastify-bridge/tsconfig.json b/src/packages/fastify-bridge/tsconfig.json new file mode 100644 index 0000000..9d4817e --- /dev/null +++ b/src/packages/fastify-bridge/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "paths": { + "#fastify-bridge/*": ["./*.ts"] + }, + "rootDir": ".", + "outDir": "./dist" + }, + "include": ["./*.ts"], + "exclude": ["./*.test.ts", "./dist"] +} From 7dbeeeda026fecfd529acb86ede03a8b62f94558 Mon Sep 17 00:00:00 2001 From: Dhemy Date: Wed, 22 Jul 2026 20:21:22 +0200 Subject: [PATCH 13/17] update workspace package lock --- package-lock.json | 706 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 706 insertions(+) diff --git a/package-lock.json b/package-lock.json index 37bf5c5..7d89109 100644 --- a/package-lock.json +++ b/package-lock.json @@ -757,6 +757,123 @@ "node": ">=18" } }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^7.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, "node_modules/@hapi/bourne": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", @@ -831,6 +948,14 @@ "resolved": "src/packages/contracts", "link": true }, + "node_modules/@koala-ts/fastify-bridge": { + "resolved": "src/packages/fastify-bridge", + "link": true + }, + "node_modules/@koala-ts/framework-bundle": { + "resolved": "src/packages/framework-bundle", + "link": true + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -890,6 +1015,13 @@ "node": ">=10" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "dev": true, + "license": "MIT" + }, "node_modules/@publint/pack": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.5.tgz", @@ -1920,6 +2052,13 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "dev": true, + "license": "MIT" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1989,6 +2128,41 @@ "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2055,6 +2229,37 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz", + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2228,6 +2433,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -2382,6 +2601,16 @@ "node": ">=22.12.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -2602,12 +2831,154 @@ "node": ">=12.0.0" } }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/fast-uri": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.1.tgz", + "integrity": "sha512-YPOs1zD5TG2+EZt+r88LwF6mclA7TPkpwMP7ZN3TO2HiHS8TXvq7QA/17iJsV9dubcLo/f8eEYqMBruyQV21hQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.10.0.tgz", + "integrity": "sha512-A9L0ziuWGQHgEEVgF3davQ9vbD93IuX+lo2IsxapQmu5b/Y/ynn9m9K5JHt9dvyJXOFc5iN0Zk5GHEOqnzhWjg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^7.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", + "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -2939,6 +3310,16 @@ "node": ">=10.13.0" } }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", @@ -3037,6 +3418,33 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3147,6 +3555,45 @@ "url": "https://opencollective.com/express" } }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -3615,6 +4062,16 @@ ], "license": "MIT" }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -3714,6 +4171,46 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "dev": true, + "license": "MIT" + }, "node_modules/postcss": { "version": "8.5.16", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", @@ -3743,6 +4240,23 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -3794,6 +4308,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true, + "license": "MIT" + }, "node_modules/raw-body": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", @@ -3809,6 +4330,16 @@ "node": ">= 0.8" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -3838,6 +4369,16 @@ "regexp-tree": "bin/regexp-tree" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -3928,6 +4469,34 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rolldown": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", @@ -3985,12 +4554,62 @@ "regexp-tree": "~0.1.1" } }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -4004,6 +4623,13 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "dev": true, + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -4117,6 +4743,16 @@ "dev": true, "license": "MIT" }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4127,6 +4763,16 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -4246,6 +4892,26 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "dev": true, + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -4308,6 +4974,16 @@ "node": ">=14.0.0" } }, + "node_modules/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -4752,6 +5428,36 @@ "node": ">=24", "npm": ">=11" } + }, + "src/packages/fastify-bridge": { + "name": "@koala-ts/fastify-bridge", + "version": "2.0.0", + "license": "Apache-2.0", + "dependencies": { + "@koala-ts/contracts": "*" + }, + "devDependencies": { + "fastify": "^5" + }, + "engines": { + "node": ">=24", + "npm": ">=11" + }, + "peerDependencies": { + "fastify": "^5" + } + }, + "src/packages/framework-bundle": { + "name": "@koala-ts/framework-bundle", + "version": "2.0.0", + "license": "Apache-2.0", + "dependencies": { + "@koala-ts/contracts": "*" + }, + "engines": { + "node": ">=24", + "npm": ">=11" + } } } } From f9b6d30dc314ac91b9034c610b3c1900db248052 Mon Sep 17 00:00:00 2001 From: Dhemy Date: Wed, 22 Jul 2026 20:25:47 +0200 Subject: [PATCH 14/17] Update package-lock.json --- package-lock.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/package-lock.json b/package-lock.json index 7d89109..7c1e2d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -956,6 +956,10 @@ "resolved": "src/packages/framework-bundle", "link": true }, + "node_modules/@koala-ts/http": { + "resolved": "src/packages/http", + "link": true + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -5458,6 +5462,15 @@ "node": ">=24", "npm": ">=11" } + }, + "src/packages/http": { + "name": "@koala-ts/http", + "version": "2.0.0", + "license": "Apache-2.0", + "engines": { + "node": ">=24", + "npm": ">=11" + } } } } From 353e328c4a4d3fb126d473e054864dd06a29a685 Mon Sep 17 00:00:00 2001 From: Dhemy Date: Wed, 22 Jul 2026 20:29:58 +0200 Subject: [PATCH 15/17] add HTTP package publish workflow --- .../http/.github/workflows/publish.yml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/packages/http/.github/workflows/publish.yml diff --git a/src/packages/http/.github/workflows/publish.yml b/src/packages/http/.github/workflows/publish.yml new file mode 100644 index 0000000..a93fae5 --- /dev/null +++ b/src/packages/http/.github/workflows/publish.yml @@ -0,0 +1,30 @@ +name: Publish + +on: + push: + tags: + - '*' + workflow_dispatch: + inputs: + release_tag: + description: Component tag that provides the package version + required: true + type: string + dry_run: + description: Validate the package without publishing it + required: true + default: true + type: boolean + +permissions: + contents: read + id-token: write + +jobs: + publish: + uses: koala-ts/framework/.github/workflows/publish-component.yml@2.x + with: + framework_ref: 2.x + release_tag: ${{ inputs.release_tag || github.ref_name }} + dry_run: ${{ inputs.dry_run || false }} + secrets: inherit From 333ba61d0912ff2558a6647759880160f28bff49 Mon Sep 17 00:00:00 2001 From: Dhemy Date: Wed, 22 Jul 2026 20:38:35 +0200 Subject: [PATCH 16/17] tighten dep-cruiser configs --- .dependency-cruiser.cjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.dependency-cruiser.cjs b/.dependency-cruiser.cjs index ff939cd..70c99d9 100644 --- a/.dependency-cruiser.cjs +++ b/.dependency-cruiser.cjs @@ -23,6 +23,7 @@ module.exports = { severity: 'error', from: { path: '^src/packages/', + pathNot: '^src/packages/fastify-bridge/', }, to: { path: '^node_modules/(?:@koa/router|koa|express|fastify)(?:/|$)', @@ -30,6 +31,9 @@ module.exports = { }, ], options: { + exclude: { + path: '(^|/)dist/', + }, doNotFollow: { path: 'node_modules', }, From de843aae42b5cb20fe0457a53055ca4f5556a268 Mon Sep 17 00:00:00 2001 From: Dhemy Date: Wed, 22 Jul 2026 20:38:42 +0200 Subject: [PATCH 17/17] reorder ci/cd operations --- .github/workflows/cd.yml | 6 +++--- .github/workflows/ci.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 582832c..51342ca 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -54,6 +54,9 @@ jobs: - name: Check code run: npm run lint + - name: Build + run: npm run build + - name: Check types run: npm run typecheck @@ -63,9 +66,6 @@ jobs: - name: Run tests run: npm run test - - name: Build - run: npm run build - - name: Check packages run: npm run publint diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 045e7e3..4f7c0f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,9 @@ jobs: - name: Check code run: npm run lint + - name: Build + run: npm run build + - name: Check types run: npm run typecheck @@ -36,8 +39,5 @@ jobs: - name: Run tests run: npm run test - - name: Build - run: npm run build - - name: Check packages run: npm run publint