From 6f95991999588f9f4205c16fb6f7e68723ee0e1b Mon Sep 17 00:00:00 2001 From: Tobias Schneider Date: Sat, 13 Feb 2021 01:41:25 +0100 Subject: [PATCH] initial commit --- .github/workflows/npm-publish.yml | 30 ++++++ LICENSE | 21 +++++ index.js | 152 ++++++++++++++++++++++++++++++ package.json | 29 ++++++ 4 files changed, 232 insertions(+) create mode 100644 .github/workflows/npm-publish.yml create mode 100644 LICENSE create mode 100644 index.js create mode 100644 package.json diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..63720ec --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,30 @@ +name: Publish NPM Package + +on: + release: + types: [created] + +jobs: + publish-npm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + registry-url: https://registry.npmjs.org/ + - run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + + publish-gpr: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 12 + registry-url: https://npm.pkg.github.com/ + - run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..32bfdb3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Tobias Schneider + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..984f629 --- /dev/null +++ b/index.js @@ -0,0 +1,152 @@ +class Router { + constructor() { + this.routes = [] + this.corsConfig = {} + } + + connect(url, ...handlers) { + return this.register('CONNECT', url, handlers) + } + + delete(url, ...handlers) { + return this.register('DELETE', url, handlers) + } + + get(url, ...handlers) { + return this.register('GET', url, handlers) + } + + head(url, ...handlers) { + return this.register('HEAD', url, handlers) + } + + options(url, ...handlers) { + return this.register('OPTIONS', url, handlers) + } + + patch(url, ...handlers) { + return this.register('PATCH', url, handlers) + } + + post(url, ...handlers) { + return this.register('POST', url, handlers) + } + + put(url, ...handlers) { + return this.register('PUT', url, handlers) + } + + trace(url, ...handlers) { + return this.register('TRACE', url, handlers) + } + + all(url, ...handlers) { + return this.register('*', url, handlers) + } + + cors(config) { + config = config || {} + this.corsConfig = { + allowOrigin: '*', + allowMethods: '*', + allowHeaders: '*', + maxAge: 86400, + optionsSuccessStatus: 204 + } + return this + } + + register(method, url, handlers) { + this.routes.push({ + method, + url, + handlers + }) + return this + } + + getRoute(request) { + const urlArr = (new URL(request.url)).pathname.split('/').filter(i => i) + return this.routes.find(r => { + const routeArr = r.url.split('/').filter(i => i) + if (![request.method, '*'].includes(r.method) || routeArr.length !== urlArr.length) + return false + const params = {} + for (let i = 0; i < routeArr.length; i++) { + if (routeArr[i] !== urlArr[i] && routeArr[i][0] !== ':') + return false + if (routeArr[i][0] === ':') + params[routeArr[i].substring(1)] = urlArr[i] + } + request.params = params + return true + }) + } + + async handle(event) { + const request = { headers: event.request.headers, method: event.request.method, url: event.request.url } + request.params = [] + if (request.method === 'OPTIONS' && Object.keys(this.corsConfig).length) { + return new Response('', { + headers: { + 'Access-Control-Allow-Origin': this.corsConfig.allowOrigin, + 'Access-Control-Allow-Methods': this.corsConfig.allowMethods, + 'Access-Control-Allow-Headers': this.corsConfig.allowHeaders, + 'Access-Control-Max-Age': this.corsConfig.maxAge + }, + status: this.corsConfig.optionsSuccessStatus + }) + } + if (['POST', 'PUT', 'PATCH'].includes(request.method)) { + try { + request.body = await event.request.json() + } catch { + try { + request.body = await event.request.text() + } catch {} + } + } + const route = this.getRoute(request) + if (!route) { + return new Response('', { + status: 404 + }) + } + const response = { headers: {} } + if (Object.keys(this.corsConfig).length) { + response.headers = { + ...response.headers, + 'Access-Control-Allow-Origin': this.corsConfig.allowOrigin, + 'Access-Control-Allow-Methods': this.corsConfig.allowMethods, + 'Access-Control-Allow-Headers': this.corsConfig.allowHeaders, + 'Access-Control-Max-Age': this.corsConfig.maxAge, + } + } + let prevIndex = -1 + try { + const runner = async index => { + if (index === prevIndex) + throw new Error('next() called multiple times') + prevIndex = index + if (typeof route.handlers[index] === 'function') + await route.handlers[index](request, response, async () => await runner(index + 1)) + } + await runner(0) + } catch(err) { + console.error(err) + return new Response('', { + status: 500 + }) + } + const headers = Object.assign({ 'Content-Type': 'application/json' }, response.headers) + if (headers['Content-Type'] === 'application/json' && typeof response.body === 'object') + response.body = JSON.stringify(response.body) + return new Response(response.body, { + status: response.status || (response.body ? 200 : 500), + headers + }) + } +} + + +module.exports = Router \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..b1f43d4 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "@tsndr/cloudflare-worker-router", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": {}, + "repository": { + "type": "git", + "url": "git+https://github.com/tsndr/cloudflare-worker-router.git" + }, + "keywords": [ + "cloudflare", + "cloudflare-worker", + "cloudflare-workers", + "express", + "expressjs", + "framework", + "middleware", + "router", + "routing", + "worker" + ], + "author": "Tobias Schneider", + "license": "MIT", + "bugs": { + "url": "https://github.com/tsndr/cloudflare-worker-router/issues" + }, + "homepage": "https://github.com/tsndr/cloudflare-worker-router#readme" +}