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', }, 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 diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..7a8c91f --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,69 @@ +# 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. + +### 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 +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 +``` 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 diff --git a/package-lock.json b/package-lock.json index 37bf5c5..7c1e2d3 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,18 @@ "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/@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", @@ -890,6 +1019,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 +2056,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 +2132,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 +2233,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 +2437,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 +2605,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 +2835,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 +3314,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 +3422,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 +3559,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 +4066,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 +4175,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 +4244,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 +4312,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 +4334,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 +4373,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 +4473,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 +4558,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 +4627,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 +4747,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 +4767,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 +4896,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 +4978,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 +5432,45 @@ "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" + } + }, + "src/packages/http": { + "name": "@koala-ts/http", + "version": "2.0.0", + "license": "Apache-2.0", + "engines": { + "node": ">=24", + "npm": ">=11" + } } } } 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/package.json b/src/packages/contracts/package.json index b2263fd..2fab6bc 100644 --- a/src/packages/contracts/package.json +++ b/src/packages/contracts/package.json @@ -10,9 +10,15 @@ "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": { + "#contracts/*": { + "types": "./*.ts", + "default": "./dist/*.js" } }, "files": [ 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" }, 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"] +} 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"] +} 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 diff --git a/src/packages/http/foundation/http-request.ts b/src/packages/http/foundation/http-request.ts new file mode 100644 index 0000000..7dfe73b --- /dev/null +++ b/src/packages/http/foundation/http-request.ts @@ -0,0 +1,37 @@ +/** Values captured from a matched HTTP route path. */ +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 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 new file mode 100644 index 0000000..e7023ee --- /dev/null +++ b/src/packages/http/foundation/http-response.ts @@ -0,0 +1,29 @@ +/** + * A declarative HTTP response header value. + */ +type HttpResponseHeaderValue = string | readonly string[]; + +/** + * Declarative HTTP response headers. + * Header names are normalized to lowercase by HTTP bridges. + */ +type HttpResponseHeaders = { + /** A response header value indexed by its normalized lowercase name. */ + readonly [name: string]: HttpResponseHeaderValue; +}; + +/** + * 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; + /** Response headers. */ + headers: HttpResponseHeaders; + /** 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..a52b2bf --- /dev/null +++ b/src/packages/http/index.ts @@ -0,0 +1,4 @@ +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 new file mode 100644 index 0000000..19ea504 --- /dev/null +++ b/src/packages/http/kernel/handle-raw-request.test.ts @@ -0,0 +1,35 @@ +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'; + +describe('Handle raw request', () => { + it('invokes the controller with http request', async () => { + const response: HttpResponse = { + status: 200, + statusText: 'OK', + headers: {}, + body: 'Hello, world!', + }; + const message: HttpRequestMessage = { + method: 'GET', + url: '/user/123', + headers: {}, + body: (async function* () {})(), + }; + + const rawRequest: RawRequest = { + message, + 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..4385f9a --- /dev/null +++ b/src/packages/http/kernel/handle-raw-request.ts @@ -0,0 +1,36 @@ +import type { HttpRequest, HttpRequestMessage } 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: HttpRequest['params']; +}>; + +/** + * The kernel input used to invoke a controller. + * + * It combines the raw HTTP message, route-matching context, and the + * controller that receives the resulting {@link HttpRequest}. + */ +export type RawRequest = Readonly<{ + /** The raw HTTP message. */ + message: HttpRequestMessage; + /** 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..98c839c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,10 +1,64 @@ +import { existsSync, readdirSync, readFileSync } from 'node:fs'; 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( + 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; + + 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]; + }), +); + export default defineConfig({ resolve: { alias: { '#koala': path.resolve(__dirname, './src'), + ...packageAliases, }, }, test: {