diff --git a/.changeset/polite-pets-dress.md b/.changeset/polite-pets-dress.md new file mode 100644 index 0000000..6b9d527 --- /dev/null +++ b/.changeset/polite-pets-dress.md @@ -0,0 +1,6 @@ +--- +"example/middleware-simple-http": minor +"example/deno-middleware-http": minor +--- + +Add new examples for middleware diff --git a/.changeset/silent-suns-invite.md b/.changeset/silent-suns-invite.md new file mode 100644 index 0000000..ad98680 --- /dev/null +++ b/.changeset/silent-suns-invite.md @@ -0,0 +1,5 @@ +--- +"@bunny.net/edgescript-sdk": minor +--- + +Add middleware for local development diff --git a/README.md b/README.md index 3500d80..69c1011 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Network for your deployed script. With `@bunny.net/edgescript-sdk` you can write a script which will work with Deno, with Node, and within our network. -``` +```typescript import * as BunnySDK from "@bunny.net/edgescript-sdk"; function sleep(ms: number): Promise { diff --git a/example/deno-middleware-http/.gitignore b/example/deno-middleware-http/.gitignore new file mode 100644 index 0000000..b11702d --- /dev/null +++ b/example/deno-middleware-http/.gitignore @@ -0,0 +1,40 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ +/dist/ +/esm/ +/esm-bunny/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.vscode + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/example/deno-middleware-http/README.md b/example/deno-middleware-http/README.md new file mode 100644 index 0000000..e69de29 diff --git a/example/deno-middleware-http/package.json b/example/deno-middleware-http/package.json new file mode 100644 index 0000000..6f7e049 --- /dev/null +++ b/example/deno-middleware-http/package.json @@ -0,0 +1,28 @@ +{ + "name": "example/deno-middleware-http", + "version": "0.0.0", + "main": "src/index.ts", + "type": "module", + "private": true, + "repository": { + "type": "git", + "url": "git+https://github.com/BunnyWay/edge-script-sdk.git" + }, + "keywords": [ + "github", + "bunny" + ], + "author": "Bunny Devs", + "license": "MIT", + "scripts": { + "lint": "deno lint", + "test": "deno test --allow-none", + "build": "echo \"No build with Deno!\"", + "dev": "deno run src/main.ts", + "release": "echo \"No release\"" + }, + "dependencies": { + "@bunny.net/edgescript-sdk": "^0.10.0" + }, + "devDependencies": {} +} diff --git a/example/deno-middleware-http/src/main.ts b/example/deno-middleware-http/src/main.ts new file mode 100644 index 0000000..8bce624 --- /dev/null +++ b/example/deno-middleware-http/src/main.ts @@ -0,0 +1,19 @@ +import * as BunnySDK from "../../../libs/bunny-sdk/esm/lib.mjs"; + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +console.log("Starting server..."); + +BunnySDK.net.http.servePullZone({ url: "https://perdu.com/" }).onOriginRequest(async (ctx) => { + const req = ctx.request; + console.log(`[INFO]: ${req.method} - ${req.url}`); + await sleep(1); + return ctx.request; +}).onOriginResponse(async (ctx) => { + const res = ctx.response; + console.log(`[INFO]: ${res.status}`); + await sleep(1); + return ctx.response; +}); diff --git a/example/deno-simple-http-page/src/main.ts b/example/deno-simple-http-page/src/main.ts index f60b3db..bb26b24 100644 --- a/example/deno-simple-http-page/src/main.ts +++ b/example/deno-simple-http-page/src/main.ts @@ -5,7 +5,7 @@ function sleep(ms: number): Promise { } console.log("Starting server..."); -BunnySDK.net.http.serve({}, async (req) => { +BunnySDK.net.http.serve(async (req) => { console.log(`[INFO]: ${req.method} - ${req.url}`); await sleep(1); return new Response("blbl"); diff --git a/example/middleware-simple-http/.gitignore b/example/middleware-simple-http/.gitignore new file mode 100644 index 0000000..b11702d --- /dev/null +++ b/example/middleware-simple-http/.gitignore @@ -0,0 +1,40 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ +/dist/ +/esm/ +/esm-bunny/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.vscode + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/example/middleware-simple-http/eslint.config.mjs b/example/middleware-simple-http/eslint.config.mjs new file mode 100644 index 0000000..851cd29 --- /dev/null +++ b/example/middleware-simple-http/eslint.config.mjs @@ -0,0 +1,12 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + + +export default [ + { files: ["**/*.{js,mjs,cjs,ts}"] }, + { languageOptions: { globals: globals.browser } }, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; + diff --git a/example/middleware-simple-http/jest.config.js b/example/middleware-simple-http/jest.config.js new file mode 100644 index 0000000..66c6b59 --- /dev/null +++ b/example/middleware-simple-http/jest.config.js @@ -0,0 +1,9 @@ +export default { + clearMocks: true, + moduleFileExtensions: ['js', 'ts'], + testEnvironment: 'node', + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.ts$': 'ts-jest' + }, +} diff --git a/example/middleware-simple-http/package.json b/example/middleware-simple-http/package.json new file mode 100644 index 0000000..b84c32d --- /dev/null +++ b/example/middleware-simple-http/package.json @@ -0,0 +1,46 @@ +{ + "name": "example/middleware-simple-http", + "version": "0.0.0", + "main": "src/index.ts", + "type": "module", + "files": [ + "dist" + ], + "private": true, + "scripts": { + "lint": "eslint src", + "test": "jest --silent --coverage", + "dev": "pnpm run build && node dist/index.js", + "build": "ncc build src/main.ts -o dist/", + "release": "echo \"No release\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/BunnyWay/edge-script-sdk.git" + }, + "keywords": [ + "github", + "bunny" + ], + "author": "Bunny Devs", + "license": "MIT", + "devDependencies": { + "@eslint/js": "^9.8.0", + "@types/jest": "^29.5.12", + "@types/node": "^22.1.0", + "@typescript-eslint/eslint-plugin": "^8.0.1", + "@typescript-eslint/parser": "^8.0.1", + "@vercel/ncc": "^0.38.1", + "esbuild": "0.23.0", + "eslint": "^9.8.0", + "globals": "^15.9.0", + "jest": "^29.5.12", + "prettier": "^3.3.3", + "ts-jest": "^29.2.4", + "typescript": "^5.5.4", + "typescript-eslint": "^8.0.1" + }, + "dependencies": { + "@bunny.net/edgescript-sdk": "workspace:*" + } +} diff --git a/example/middleware-simple-http/src/main.ts b/example/middleware-simple-http/src/main.ts new file mode 100644 index 0000000..73233fb --- /dev/null +++ b/example/middleware-simple-http/src/main.ts @@ -0,0 +1,19 @@ +import * as BunnySDK from "@bunny.net/edgescript-sdk"; + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +console.log("Starting server..."); + +BunnySDK.net.http.servePullZone({ url: "https://perdu.com/" }).onOriginRequest(async (ctx) => { + const req = ctx.request; + console.log(`[INFO]: ${req.method} - ${req.url}`); + await sleep(1); + return ctx.request; +}).onOriginResponse(async (ctx) => { + const res = ctx.response; + console.log(`[INFO]: ${res.status}`); + await sleep(1); + return ctx.response; +}); diff --git a/example/middleware-simple-http/tests/empty.test.ts b/example/middleware-simple-http/tests/empty.test.ts new file mode 100644 index 0000000..8791bfb --- /dev/null +++ b/example/middleware-simple-http/tests/empty.test.ts @@ -0,0 +1,8 @@ +// import { jest } from '@jest/globals'; + +describe('empty', () => { + test('empty test', async () => { + expect(true).toBe(true); + }); + +}); diff --git a/example/middleware-simple-http/tsconfig.json b/example/middleware-simple-http/tsconfig.json new file mode 100644 index 0000000..37d0f8d --- /dev/null +++ b/example/middleware-simple-http/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "nodenext", + "lib": [ + "esnext" + ], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "allowSyntheticDefaultImports": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "downlevelIteration": true, + "forceConsistentCasingInFileNames": true, + "suppressExcessPropertyErrors": false, + "declaration": true, + "sourceMap": false, + "noImplicitAny": false, + "noEmitOnError": false, + "esModuleInterop": true, + "skipLibCheck": true + }, + "exclude": [ + "node_modules", + "**/*.test.*" + ] +} diff --git a/libs/bunny-sdk/src/net/serve.ts b/libs/bunny-sdk/src/net/serve.ts index 3a7060e..a3c22f7 100644 --- a/libs/bunny-sdk/src/net/serve.ts +++ b/libs/bunny-sdk/src/net/serve.ts @@ -3,7 +3,16 @@ import * as NodeImpl from "./_impl/node/serve.ts"; import { TcpListener } from "./tcp.ts"; import * as Tcp from "./tcp.ts"; import * as Ip from "./ip.ts"; -import * as SocketAddr from './socket_addr.ts'; +import * as SocketAddr from "./socket_addr.ts"; + +function isResponse(value: unknown) { + return value instanceof Response; +} + +function isRequest(value: unknown) { + return value instanceof Request; +} + /** * A handler for HTTP Requests. @@ -17,24 +26,38 @@ type ServeHandler = {} & unknown; * Serves HTTP requests on the given [TcpListener] */ -function is_port_and_hostname(value: unknown): value is { port: number; hostname: string; } { +function is_port_and_hostname( + value: unknown, +): value is { port: number; hostname: string } { if (typeof value === "object" && value !== null) { const port = value["port"]; - return port !== undefined && typeof port === "number" && value["hostname"] !== undefined; + return port !== undefined && typeof port === "number" && + value["hostname"] !== undefined; } return false; } +/** + * Serves HTTP requests with the provided handler. + */ function serve(handler: ServerHandler): ServeHandler; -function serve(listener: { port: number; hostname: string; }, handler: ServerHandler): ServeHandler; +function serve( + listener: { port: number; hostname: string }, + handler: ServerHandler, +): ServeHandler; function serve(listener: TcpListener, handler: ServerHandler): ServeHandler; -function serve(listener: ServerHandler | { port: number; hostname: string; } | TcpListener, handler?: ServerHandler): ServeHandler { +function serve( + listener: ServerHandler | { port: number; hostname: string } | TcpListener, + handler?: ServerHandler, +): ServeHandler { let raw_handler: ServerHandler | undefined; let raw_listener: TcpListener; if (is_port_and_hostname(listener)) { - const addr = SocketAddr.v4.tryFromString(`${listener.hostname}:${listener.port}`); + const addr = SocketAddr.v4.tryFromString( + `${listener.hostname}:${listener.port}`, + ); if (addr instanceof Error) { throw addr; } @@ -82,8 +105,171 @@ function serve(listener: ServerHandler | { port: number; hostname: string; } | T } } -export { - serve, - ServeHandler, - ServerHandler, +type PullZoneHandlerOptions = { + url: string; +}; + +type OriginRequestContext = { + request: Request, +}; + +type OriginResponseContext = { + request: Request, + response: Response, +}; + +type PullZoneHandler = { + /** + * Add a Middleware for the requests being processed. + */ + onOriginRequest: (middleware: ( + ctx: OriginRequestContext, + ) => Promise | Promise) => PullZoneHandler; + + /** + * Add a Middleware for the response being processed. + */ + onOriginResponse: (middleware: ( + ctx: OriginResponseContext, + ) => Promise) => PullZoneHandler; +}; + +/** + * Serves HTTP requests for a PullZone + * + * If you have an associated PullZone within Bunny, we'll use it on production + * and for local development you can configure it with the `url` option. + */ +function servePullZone(options: PullZoneHandlerOptions): PullZoneHandler; +function servePullZone( + listener: { port: number; hostname: string }, + options: PullZoneHandlerOptions, +): PullZoneHandler; +function servePullZone( + listener: TcpListener, + options: PullZoneHandlerOptions, +): PullZoneHandler; +function servePullZone( + listener?: + | PullZoneHandlerOptions + | { port: number; hostname: string } + | TcpListener, + options?: PullZoneHandlerOptions, +): PullZoneHandler { + let raw_listener: TcpListener; + let raw_options: PullZoneHandlerOptions = { + url: "https://bunny.net" + }; + + if (options) { + raw_options = options; + } + + if (is_port_and_hostname(listener)) { + const addr = SocketAddr.v4.tryFromString( + `${listener.hostname}:${listener.port}`, + ); + if (addr instanceof Error) { + throw addr; + } + raw_listener = Tcp.bind(addr); + } else if (Tcp.isTcpListener(listener)) { + raw_listener = listener; + } else { + if (listener) { + raw_options = listener; + } + raw_listener = Tcp.unstable_new(); + } + + const onOriginRequestMiddleware: Array<( + ctx: OriginRequestContext, + ) => Promise | Promise | undefined> = []; + const onOriginResponseMiddleware: Array<( + ctx: OriginResponseContext, + ) => Promise | undefined> = []; + + const platform = internal_getPlatform(); + + switch (platform.runtime) { + case "bunny": { + Bunny.v1.registerMiddlewares({ onOriginRequest: onOriginRequestMiddleware, onOriginResponse: onOriginResponseMiddleware }); + break; + } + default: { + const middlewareHandler: ServerHandler = async (req) => { + + let mutableRequest = new Request(raw_options.url, { ...req as unknown as RequestInit }); + + // Request Middleware + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [_, mid] of onOriginRequestMiddleware.entries()) { + const reqOrResponse = await mid({ request: mutableRequest }); + if (isResponse(reqOrResponse)) { + return reqOrResponse; + } + if (isRequest(reqOrResponse)) { + mutableRequest = reqOrResponse; + } + } + + const prevResponse = await fetch(mutableRequest); + + const headers = new Headers(); + for (const [key, value] of prevResponse.headers.entries()) { + headers.set(key, value); + } + + let response: Response; + + // Only for node + switch (platform.runtime) { + case "node": { + if (headers.get("content-type") === "text/html" && prevResponse.body !== null) { + const body = await prevResponse.text(); + headers.delete("content-encoding"); + response = new Response(body, { headers }); + } else { + response = new Response(prevResponse.body, { ...prevResponse, headers }); + } + + break; + } + default: { + response = new Response(prevResponse.body, { ...prevResponse, headers }); + } + } + + // Response Middleware + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const [_, mid] of onOriginResponseMiddleware.entries()) { + const reqOrResponse = await mid({ request: mutableRequest, response }); + if (isResponse(reqOrResponse)) { + response = reqOrResponse; + } + } + + return response; + }; + + serve(raw_listener, middlewareHandler); + } + } + + const pullzoneHandler = ({ + }) as PullZoneHandler; + + pullzoneHandler.onOriginResponse = (middleware) => { + onOriginResponseMiddleware.push(middleware); + return pullzoneHandler; + }; + + pullzoneHandler.onOriginRequest = (middleware) => { + onOriginRequestMiddleware.push(middleware); + return pullzoneHandler; + }; + + return pullzoneHandler; } + +export { serve, ServeHandler, servePullZone, ServerHandler }; diff --git a/libs/bunny-sdk/tsconfig.json b/libs/bunny-sdk/tsconfig.json index 412964d..16696aa 100644 --- a/libs/bunny-sdk/tsconfig.json +++ b/libs/bunny-sdk/tsconfig.json @@ -3,7 +3,8 @@ "target": "esnext", "module": "nodenext", "lib": [ - "esnext" + "esnext", + "dom" ], "outDir": "./dist", "rootDir": "./src", diff --git a/libs/bunny-sdk/types/bunny.d.ts b/libs/bunny-sdk/types/bunny.d.ts index b98d659..18314fa 100644 --- a/libs/bunny-sdk/types/bunny.d.ts +++ b/libs/bunny-sdk/types/bunny.d.ts @@ -8,6 +8,17 @@ declare namespace Bunny { * Serve function */ serve: (handler: (request: Request) => Response | Promise) => void, + /** + * Serve PullZone function, to leverage middlewares + */ + registerMiddlewares: (middlewares: { + onOriginRequest: Array<( + ctx: { request: Request }, + ) => Promise | Promise | undefined> + onOriginResponse: Array<( + ctx: { request: Request, response: Response }, + ) => Promise | Promise | undefined> + }) => void, }; export const v1: BunnySDKV1; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6b0522..b24232b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,12 +12,67 @@ importers: specifier: ^2.26.2 version: 2.27.7 + example/deno-middleware-http: + dependencies: + '@bunny.net/edgescript-sdk': + specifier: ^0.10.0 + version: link:../../libs/bunny-sdk + example/deno-simple-http-page: dependencies: '@bunny.net/edgescript-sdk': specifier: ^0.10.0 version: link:../../libs/bunny-sdk + example/middleware-simple-http: + dependencies: + '@bunny.net/edgescript-sdk': + specifier: workspace:* + version: link:../../libs/bunny-sdk + devDependencies: + '@eslint/js': + specifier: ^9.8.0 + version: 9.9.0 + '@types/jest': + specifier: ^29.5.12 + version: 29.5.12 + '@types/node': + specifier: ^22.1.0 + version: 22.2.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.0.1 + version: 8.1.0(@typescript-eslint/parser@8.1.0)(eslint@9.9.0)(typescript@5.5.4) + '@typescript-eslint/parser': + specifier: ^8.0.1 + version: 8.1.0(eslint@9.9.0)(typescript@5.5.4) + '@vercel/ncc': + specifier: ^0.38.1 + version: 0.38.1 + esbuild: + specifier: 0.23.0 + version: 0.23.0 + eslint: + specifier: ^9.8.0 + version: 9.9.0 + globals: + specifier: ^15.9.0 + version: 15.9.0 + jest: + specifier: ^29.5.12 + version: 29.7.0(@types/node@22.2.0) + prettier: + specifier: ^3.3.3 + version: 3.3.3 + ts-jest: + specifier: ^29.2.4 + version: 29.2.4(@babel/core@7.25.2)(esbuild@0.23.0)(jest@29.7.0)(typescript@5.5.4) + typescript: + specifier: ^5.5.4 + version: 5.5.4 + typescript-eslint: + specifier: ^8.0.1 + version: 8.1.0(eslint@9.9.0)(typescript@5.5.4) + example/simple-http-page: dependencies: '@bunny.net/edgescript-sdk': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 447ad23..b3f6504 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,5 @@ packages: - 'libs/bunny-sdk' - 'example/simple-http-page' - 'example/deno-simple-http-page' + - 'example/deno-middleware-http' + - 'example/middleware-simple-http'