diff --git a/package-lock.json b/package-lock.json index 41206e5e..c3f3bc06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@types/express": "4.17.21", "@types/koa": "2.13.12", "@types/node": "18.16.5", + "@types/polka": "0.5.7", "@types/supertest": "6.0.1", "@typescript-eslint/eslint-plugin": "6.15.0", "@typescript-eslint/parser": "6.15.0", @@ -53,6 +54,7 @@ "glob": "10.3.10", "husky": "8.0.3", "koa": "2.15.0", + "polka": "0.5.2", "prettier": "3.1.1", "stream-mock": "2.0.5", "supertest": "6.3.3", @@ -432,6 +434,15 @@ "node": ">=14" } }, + "node_modules/@arr/every": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@arr/every/-/every-1.0.1.tgz", + "integrity": "sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@azure/functions": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-3.5.1.tgz", @@ -3391,6 +3402,12 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@polka/url": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-0.5.0.tgz", + "integrity": "sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw==", + "dev": true + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "dev": true, @@ -3824,6 +3841,18 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/polka": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@types/polka/-/polka-0.5.7.tgz", + "integrity": "sha512-TH8CDXM8zoskPCNmWabtK7ziGv9Q21s4hMZLVYK5HFEfqmGXBqq/Wgi7jNELWXftZK/1J/9CezYa06x1RKeQ+g==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/express-serve-static-core": "*", + "@types/node": "*", + "@types/trouter": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.7", "dev": true, @@ -3896,6 +3925,12 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/trouter": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/trouter/-/trouter-3.1.4.tgz", + "integrity": "sha512-4YIL/2AvvZqKBWenjvEpxpblT2KGO6793ipr5QS7/6DpQ3O3SwZGgNGWezxf3pzeYZc24a2pJIrR/+Jxh/wYNQ==", + "dev": true + }, "node_modules/@types/uuid": { "version": "8.3.4", "dev": true, @@ -9556,6 +9591,18 @@ "node": ">= 12" } }, + "node_modules/matchit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/matchit/-/matchit-1.1.0.tgz", + "integrity": "sha512-+nGYoOlfHmxe5BW5tE0EMJppXEwdSf8uBA1GTZC7Q77kbT35+VKLYJMzVNWCHSsga1ps1tPYFtFyvxvKzWVmMA==", + "dev": true, + "dependencies": { + "@arr/every": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/md5": { "version": "2.3.0", "dev": true, @@ -10465,6 +10512,16 @@ "pathe": "^1.1.0" } }, + "node_modules/polka": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/polka/-/polka-0.5.2.tgz", + "integrity": "sha512-FVg3vDmCqP80tOrs+OeNlgXYmFppTXdjD5E7I4ET1NjvtNmQrb1/mJibybKkb/d4NA7YWAr1ojxuhpL3FHqdlw==", + "dev": true, + "dependencies": { + "@polka/url": "^0.5.0", + "trouter": "^2.0.1" + } + }, "node_modules/postcss": { "version": "8.4.32", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", @@ -12182,6 +12239,18 @@ "tree-kill": "cli.js" } }, + "node_modules/trouter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/trouter/-/trouter-2.0.1.tgz", + "integrity": "sha512-kr8SKKw94OI+xTGOkfsvwZQ8mWoikZDd2n8XZHjJVZUARZT+4/VV6cacRS6CLsH9bNm+HFIPU1Zx4CnNnb4qlQ==", + "dev": true, + "dependencies": { + "matchit": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", diff --git a/package.json b/package.json index e31b0685..da046f41 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,8 @@ "lambda edge", "alb", "lambda", + "lambda streaming", + "response streaming", "apollo server", "express", "koa", @@ -69,7 +71,8 @@ "digital ocean functions", "digital ocean serverless", "gcp", - "google cloud functions" + "google cloud functions", + "polka" ], "bugs": { "url": "https://github.com/H4ad/serverless-adapter/issues" @@ -97,6 +100,7 @@ "@types/express": "4.17.21", "@types/koa": "2.13.12", "@types/node": "18.16.5", + "@types/polka": "0.5.7", "@types/supertest": "6.0.1", "@typescript-eslint/eslint-plugin": "6.15.0", "@typescript-eslint/parser": "6.15.0", @@ -120,6 +124,7 @@ "glob": "10.3.10", "husky": "8.0.3", "koa": "2.15.0", + "polka": "0.5.2", "prettier": "3.1.1", "stream-mock": "2.0.5", "supertest": "6.3.3", @@ -530,6 +535,26 @@ "default": "./lib/frameworks/lazy/index.cjs" } }, + "./frameworks/polka": { + "import": { + "types": "./lib/frameworks/polka/index.d.ts", + "default": "./lib/frameworks/polka/index.mjs" + }, + "require": { + "types": "./lib/frameworks/polka/index.d.cts", + "default": "./lib/frameworks/polka/index.cjs" + } + }, + "./lib/frameworks/polka": { + "import": { + "types": "./lib/frameworks/polka/index.d.ts", + "default": "./lib/frameworks/polka/index.mjs" + }, + "require": { + "types": "./lib/frameworks/polka/index.d.cts", + "default": "./lib/frameworks/polka/index.cjs" + } + }, "./frameworks/trpc": { "import": { "types": "./lib/frameworks/trpc/index.d.ts", diff --git a/src/frameworks/polka/index.ts b/src/frameworks/polka/index.ts new file mode 100644 index 00000000..caa8c0ae --- /dev/null +++ b/src/frameworks/polka/index.ts @@ -0,0 +1 @@ +export * from './polka.framework'; diff --git a/src/frameworks/polka/polka.framework.ts b/src/frameworks/polka/polka.framework.ts new file mode 100644 index 00000000..e478f8f3 --- /dev/null +++ b/src/frameworks/polka/polka.framework.ts @@ -0,0 +1,26 @@ +//#region Imports + +import type { IncomingMessage, ServerResponse } from 'http'; +import polka, { type Polka } from 'polka'; +import type { FrameworkContract } from '../../contracts'; + +//#endregion + +/** + * The framework that forwards requests to polka handler + * + * @breadcrumb Frameworks / PolkaFramework + * @public + */ +export class PolkaFramework implements FrameworkContract { + /** + * {@inheritDoc} + */ + sendRequest( + app: Polka, + request: IncomingMessage, + response: ServerResponse, + ): void { + app.handler(request as polka.Request, response); + } +} diff --git a/src/index.doc.ts b/src/index.doc.ts index 036fb7a4..00122c86 100644 --- a/src/index.doc.ts +++ b/src/index.doc.ts @@ -17,6 +17,7 @@ export * from './frameworks/fastify'; export * from './frameworks/koa'; export * from './frameworks/hapi'; export * from './frameworks/lazy'; +export * from './frameworks/polka'; export * from './frameworks/trpc'; export * from './handlers/azure'; export * from './handlers/aws'; diff --git a/test/frameworks/body-parser.framework.spec.ts b/test/frameworks/body-parser.framework.spec.ts index 69f31b9b..785fd97c 100644 --- a/test/frameworks/body-parser.framework.spec.ts +++ b/test/frameworks/body-parser.framework.spec.ts @@ -1,18 +1,18 @@ -import type { ServerResponse } from 'http'; import * as trpc from '@trpc/server'; import type { Options } from 'body-parser'; import express, { type Express } from 'express'; import fastify from 'fastify'; import Application from 'koa'; -import { SpyInstance, describe, expect, it, vitest } from 'vitest'; +import { type SpyInstance, describe, expect, it, vitest } from 'vitest'; +import polka from 'polka'; import { - FrameworkContract, + type FrameworkContract, ServerlessRequest, ServerlessResponse, waitForStreamComplete, } from '../../src'; import { - BodyParserOptions, + type BodyParserOptions, JsonBodyParserFramework, RawBodyParserFramework, TextBodyParserFramework, @@ -22,7 +22,11 @@ import { ExpressFramework } from '../../src/frameworks/express'; import { FastifyFramework } from '../../src/frameworks/fastify'; import { setNoOpForContentType } from '../../src/frameworks/fastify/helpers/no-op-content-parser'; import { KoaFramework } from '../../src/frameworks/koa'; -import { TrpcAdapterContext, TrpcFramework } from '../../src/frameworks/trpc'; +import { + type TrpcAdapterContext, + TrpcFramework, +} from '../../src/frameworks/trpc'; +import { PolkaFramework } from '../../src/frameworks/polka'; type BodyParserTest = { name: string; @@ -35,7 +39,14 @@ type BodyParserTest = { notExpectedBody?: any; status: number; expectSendRequestOfTheFrameworkToBeCalled: boolean; - skipFrameworks?: ('express' | 'fastify' | 'koa' | 'hapi' | 'trpc')[]; + skipFrameworks?: ( + | 'express' + | 'fastify' + | 'koa' + | 'hapi' + | 'trpc' + | 'polka' + )[]; }; const bodyParserOptions: BodyParserTest[] = [ @@ -259,7 +270,7 @@ describe('BodyParserFramework', () => { res.send('ok'); }); - app.use((err, __, res: ServerResponse, _) => { + app.use((err, __, res, _) => { res.emit('error', err); }); @@ -290,7 +301,7 @@ describe('BodyParserFramework', () => { res.send('ok'); }); - app.setErrorHandler((err, req, reply) => { + app.setErrorHandler((err, _req, reply) => { reply.raw.emit('error', err); }); @@ -387,6 +398,28 @@ describe('BodyParserFramework', () => { } }); + describe('polka', () => { + for (const bodyParserTest of bodyParserOptions) { + const itFn = bodyParserTest?.skipFrameworks?.includes('polka') + ? it.skip + : it; + + itFn(bodyParserTest.name, async () => { + const app = polka(); + + app.post('/body', (req, res) => { + if (bodyParserTest.expectedBody) + expect(req.body).toEqual(bodyParserTest.expectedBody); + else expect(req.body).not.toEqual(bodyParserTest.notExpectedBody); + + res.end('ok'); + }); + + await handleRestExpects(app, new PolkaFramework(), bodyParserTest); + }); + } + }); + it('should handle correctly on wrong content-encoding', async () => { const app = express(); diff --git a/test/frameworks/cors.framework.spec.ts b/test/frameworks/cors.framework.spec.ts index 1799d6e7..bf4ddce8 100644 --- a/test/frameworks/cors.framework.spec.ts +++ b/test/frameworks/cors.framework.spec.ts @@ -2,19 +2,24 @@ import * as trpc from '@trpc/server'; import express from 'express'; import fastify from 'fastify'; import Application from 'koa'; -import { SpyInstance, describe, expect, it, vitest } from 'vitest'; +import { type SpyInstance, describe, expect, it, vitest } from 'vitest'; +import polka from 'polka'; import { - BothValueHeaders, - FrameworkContract, + type BothValueHeaders, + type FrameworkContract, ServerlessRequest, ServerlessResponse, waitForStreamComplete, } from '../../src'; -import { CorsFramework, CorsFrameworkOptions } from '../../src/frameworks/cors'; +import { + CorsFramework, + type CorsFrameworkOptions, +} from '../../src/frameworks/cors'; import { ExpressFramework } from '../../src/frameworks/express'; import { FastifyFramework } from '../../src/frameworks/fastify'; import { KoaFramework } from '../../src/frameworks/koa'; import { TrpcFramework } from '../../src/frameworks/trpc'; +import { PolkaFramework } from '../../src/frameworks/polka'; type CorsTest = { name: string; @@ -287,4 +292,16 @@ describe('CorsFramework', () => { }); } }); + + describe('polka', () => { + for (const corsTest of corsOptions) { + it(`${corsTest.method}: ${corsTest.name}`, async () => { + const app = polka(); + + app.get('/', (_, res) => res.end('ok')); + + await handleRestExpects(app, new PolkaFramework(), corsTest); + }); + } + }); }); diff --git a/test/frameworks/polka.framework.spec.ts b/test/frameworks/polka.framework.spec.ts new file mode 100644 index 00000000..159737d7 --- /dev/null +++ b/test/frameworks/polka.framework.spec.ts @@ -0,0 +1,36 @@ +import { describe } from 'vitest'; +import polka, { type Polka } from 'polka'; +import { PolkaFramework } from '../../src/frameworks/polka'; +import { type TestRouteBuilderHandler, createTestSuiteFor } from './utils'; + +function createHandler( + method: 'get' | 'post' | 'delete' | 'put', +): TestRouteBuilderHandler { + return (app, path, handler) => { + app[method](path, (request, response) => { + const [statusCode, resultBody, headers] = handler( + request.headers, + request.body, + ); + + for (const header of Object.keys(headers)) + response.setHeader(header, headers[header]); + + response.statusCode = statusCode; + response.end(JSON.stringify(resultBody)); + }); + }; +} + +describe(PolkaFramework.name, () => { + createTestSuiteFor( + () => new PolkaFramework(), + () => polka(), + { + get: createHandler('get'), + delete: createHandler('delete'), + post: createHandler('post'), + put: createHandler('put'), + }, + ); +}); diff --git a/test/issues/issue-165/transfer-encoding-chunked-support.spec.ts b/test/issues/issue-165/transfer-encoding-chunked-support.spec.ts index a5804428..ab25924a 100644 --- a/test/issues/issue-165/transfer-encoding-chunked-support.spec.ts +++ b/test/issues/issue-165/transfer-encoding-chunked-support.spec.ts @@ -3,6 +3,7 @@ import { Readable } from 'node:stream'; import { describe, expect, it } from 'vitest'; import express from 'express'; import fastify from 'fastify'; +import polka from 'polka'; import { ServerlessAdapter } from '../../../src'; import { DefaultHandler } from '../../../src/handlers/default'; import { PromiseResolver } from '../../../src/resolvers/promise'; @@ -10,6 +11,7 @@ import { ExpressFramework } from '../../../src/frameworks/express'; import { AlbAdapter } from '../../../src/adapters/aws'; import { createAlbEvent } from '../../adapters/aws/utils/alb-event'; import { FastifyFramework } from '../../../src/frameworks/fastify'; +import { PolkaFramework } from '../../../src/frameworks/polka'; const expectedResult = 'INITIAL PAYLOAD RESPONSE\nFINAL PAYLOAD RESPONSE\n'; @@ -79,4 +81,34 @@ describe('Issue 165: cannot handle transfer-encoding: chunked', () => { expect(result.body).toEqual(expectedResult); expect(result.headers['content-length'], expectedResult.length.toString()); }); + + it('polka: should handle transfer-encoding: chunked', async () => { + const app = polka(); + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + app.get('/chunked-response', async (_req, res) => { + // Send headers right away + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Transfer-Encoding', 'chunked'); + res.statusCode = 200; + + res.write('INITIAL PAYLOAD RESPONSE\n'); + await setTimeout(50); + res.end('FINAL PAYLOAD RESPONSE\n'); + }); + + const albEvent = createAlbEvent('GET', '/chunked-response'); + + const handler = ServerlessAdapter.new(app) + .setHandler(new DefaultHandler()) + .setFramework(new PolkaFramework()) + .setResolver(new PromiseResolver()) + .addAdapter(new AlbAdapter()) + .build(); + + const result = await handler(albEvent, {}); + + expect(result.body).toEqual(expectedResult); + expect(result.headers['content-length'], expectedResult.length.toString()); + }); }); diff --git a/tsup.config.ts b/tsup.config.ts index af2382cf..d2b5a4e8 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -20,6 +20,7 @@ const frameworks = [ 'hapi', 'koa', 'lazy', + 'polka', 'trpc', ]; diff --git a/www/docs/main/frameworks/polka.mdx b/www/docs/main/frameworks/polka.mdx new file mode 100644 index 00000000..0870c46b --- /dev/null +++ b/www/docs/main/frameworks/polka.mdx @@ -0,0 +1,44 @@ +--- +title: Polka +description: See more about how to integrate with Polka. +--- + +First, you need to ensure you have the libs installed, so run this code: + +```bash +npm i --save polka +npm i --save-dev @types/polka +``` + +Then, you need you just need to use the [PolkaFramework](../../api/Frameworks/PolkaFramework) when you create your adapter, like: + +```ts title="index.ts" +import { ServerlessAdapter } from '@h4ad/serverless-adapter'; +import { PolkaFramework } from '@h4ad/serverless-adapter/frameworks/polka'; + +const Polka = require('Polka'); + +const app = Polka(); +export const handler = ServerlessAdapter.new(app) + .setFramework(new PolkaFramework()) + // continue to set the other options here. + //.setHandler(new DefaultHandler()) + //.setResolver(new PromiseResolver()) + //.addAdapter(new AlbAdapter()) + //.addAdapter(new SQSAdapter()) + //.addAdapter(new SNSAdapter()) + // after put all methods necessary, just call the build method. + .build(); +``` + +:::tip + +Is your application instance creation asynchronous? Look the [LazyFramework](./helpers/lazy) which helps you in asynchronous startup. + +::: + +:::tip + +Need to deal with CORS? See [CorsFramework](./helpers/cors) which helps you to add correct headers. + +::: diff --git a/www/sidebars.js b/www/sidebars.js index 52eafa28..1cba643f 100644 --- a/www/sidebars.js +++ b/www/sidebars.js @@ -140,6 +140,7 @@ const sidebars = { 'main/frameworks/hapi', 'main/frameworks/koa', 'main/frameworks/nestjs', + 'main/frameworks/polka', 'main/frameworks/trpc', { type: 'category', @@ -154,7 +155,7 @@ const sidebars = { dirName: 'main/frameworks/helpers', }, ], - } + }, ], }, { @@ -176,9 +177,9 @@ const sidebars = { 'main/advanced/adapters/introduction', 'main/advanced/adapters/creating-an-adapter', ], - } + }, ], - } + }, ], api: [ {