├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── bin ├── lint.sh └── test.sh ├── example ├── context-extension.js ├── context.js ├── cookie.js ├── custom-transport-basis.js ├── echo.js ├── file-stream.js ├── hello-world.js ├── push.js └── router.js ├── package-lock.json ├── package.json ├── packages ├── - │ └── .npmignore ├── electron-adapter │ ├── .npmignore │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ │ └── index.js │ └── test │ │ └── index.spec.js ├── electron │ ├── .npmignore │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ │ └── index.js │ └── test │ │ └── index.spec.js ├── flow │ ├── .npmignore │ ├── .testuprc │ ├── license │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ │ └── index.js │ └── test │ │ └── flow.spec.js ├── fs │ ├── .npmignore │ ├── license │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── custom-fs.js │ │ └── index.js │ └── test │ │ └── index.spec.js ├── http-adapter │ ├── .jsdoc.json │ ├── .npmignore │ ├── LICENSE │ ├── changelog.md │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── deps │ │ │ ├── web-streams-polyfill.js │ │ │ └── web-streams-polyfill.js.map │ │ └── index.js │ └── test │ │ ├── http.spec.js │ │ ├── http2.spec.js │ │ ├── index.js │ │ └── test-suite.js ├── http │ ├── .jsdoc.json │ ├── .npmignore │ ├── LICENSE │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ │ └── index.js │ └── test │ │ └── index.spec.js ├── http2 │ ├── .npmignore │ ├── .testuprc │ ├── changelog.md │ ├── examples │ │ ├── assets │ │ │ ├── index.html │ │ │ └── style.css │ │ └── push.js │ ├── license │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ │ └── index.js │ └── test │ │ └── index.spec.js ├── https │ ├── .jsdoc.json │ ├── .npmignore │ ├── LICENSE │ ├── changelog.md │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ │ └── index.js │ └── test │ │ ├── fixtures │ │ ├── cert.pem │ │ └── key.pem │ │ └── index.spec.js ├── https2 │ ├── .npmignore │ ├── .testuprc │ ├── changelog.md │ ├── examples │ │ ├── assets │ │ │ ├── index.html │ │ │ └── style.css │ │ └── push.js │ ├── license │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ │ └── index.js │ ├── test │ │ ├── fixtures │ │ │ ├── cert.pem │ │ │ └── key.pem │ │ └── index.spec.js │ └── utils │ │ └── ssl.sh ├── node-stream-utils │ ├── .npmignore │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ │ └── index.js │ └── test │ │ └── index.spec.js ├── plant │ ├── .jsdoc.json │ ├── .npmignore │ ├── LICENSE │ ├── changelog.md │ ├── dev │ │ └── cover.png │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── handlers │ │ │ └── cookie-handler.js │ │ ├── headers.js │ │ ├── index.js │ │ ├── peer.js │ │ ├── request.js │ │ ├── response.js │ │ ├── route.js │ │ ├── server.js │ │ ├── socket.js │ │ ├── uri.js │ │ └── util │ │ │ ├── mime-type-matcher.js │ │ │ ├── stream.js │ │ │ └── type-header.js │ └── test │ │ ├── fetch.spec.js │ │ ├── headers.spec.js │ │ ├── index.js │ │ ├── request.spec.js │ │ ├── response.spec.js │ │ ├── route.spec.js │ │ ├── server.spec.js │ │ ├── socket.spec.js │ │ ├── uri.spec.js │ │ └── utils │ │ └── readable-stream.js ├── router │ ├── .npmignore │ ├── license │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ │ └── index.js │ └── test │ │ └── router.spec.js ├── test-http │ ├── .npmignore │ ├── .testuprc │ ├── license │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── fetch-http.js │ │ ├── fetch-http2.js │ │ ├── fetch-https.js │ │ ├── http.js │ │ ├── http2.js │ │ ├── https.js │ │ ├── https2.js │ │ └── index.js │ └── test │ │ ├── http.spec.js │ │ ├── http2.spec.js │ │ ├── https.spec.js │ │ ├── https2.spec.js │ │ └── ssl │ │ ├── cert.pem │ │ └── key.pem └── vfs │ ├── .jsdoc.json │ ├── .npmignore │ ├── license │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── src │ └── index.js │ └── test │ ├── index.spec.js │ └── lib │ └── fs.js └── readme.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [{*.js,*.json,.travis.yml}] 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumkin/plant/061f376dbdee578ce141abec39a5bc06c6a86f97/.eslintignore -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "parserOptions": { 7 | "sourceType": "module", 8 | "ecmaVersion": 9 9 | }, 10 | "extends": "eslint:recommended", 11 | "rules": { 12 | "block-scoped-var": 2, 13 | "brace-style": [2, "stroustrup"], 14 | "camelcase": 0, 15 | "curly": 2, 16 | "eol-last": 2, 17 | "eqeqeq": [2, "smart"], 18 | "max-depth": [1, 3], 19 | "max-statements": [1, 40], 20 | "max-len": 0, 21 | "new-cap": 0, 22 | "no-extend-native": 2, 23 | "no-mixed-spaces-and-tabs": 2, 24 | "no-multiple-empty-lines": [2, {"max": 1}], 25 | "no-prototype-builtins": 0, 26 | "no-trailing-spaces": 2, 27 | "no-use-before-define": 0, 28 | "no-undefined": 2, 29 | "no-undef": 2, 30 | "no-unused-vars": 1, 31 | "indent": [2, 2, { "MemberExpression": 0 }], 32 | "quotes": [2, "single", {"avoidEscape": true}], 33 | "semi": [2, "never"], 34 | "keyword-spacing": [2, {"before": true, "after": true}], 35 | "object-curly-spacing": [2, "never"], 36 | "array-bracket-spacing": [2, "never"], 37 | "computed-property-spacing": 1, 38 | "space-unary-ops": 0, 39 | "valid-jsdoc": 1, 40 | "no-nested-ternary": 2, 41 | "no-underscore-dangle": 0, 42 | "comma-dangle": [1, "always-multiline"], 43 | "no-shadow": 1 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | *.pid 4 | 5 | build/ 6 | dist/ 7 | docs/ 8 | local/ 9 | tmp/ 10 | 11 | # OS files 12 | .DS_Store 13 | [Tt]humbs.db 14 | Desktop.ini 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "node" 5 | - "12" 6 | - "11" 7 | branches: 8 | only: 9 | - master 10 | script: 11 | - npm i --global eslint 12 | - ./bin/test.sh install 13 | - ./bin/test.sh lint 14 | - ./bin/test.sh test 15 | -------------------------------------------------------------------------------- /bin/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | FILES=`git diff --cached --name-status | egrep '^(M|A)' | egrep '\.js$' | awk '{ print $2 }'` 6 | 7 | for FILE in $FILES; 8 | do 9 | echo "LINT: $FILE" 10 | git show :$FILE | npx eslint --stdin --stdin-filename $FILE 11 | done 12 | -------------------------------------------------------------------------------- /bin/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | CWD=$PWD 6 | 7 | run_install() { 8 | local DIR=$1 9 | 10 | echo "INSTALL: $DIR" 11 | 12 | cd "$CWD/packages/$DIR" 13 | 14 | npm i . 15 | } 16 | 17 | run_test() { 18 | local DIRNAME=$1 19 | local DIR="$CWD/packages/$DIRNAME" 20 | 21 | echo "TEST: $DIRNAME ($DIR)" 22 | 23 | cd "$DIR" 24 | 25 | npm test 26 | } 27 | 28 | run_lint() { 29 | local DIRNAME=$1 30 | local DIR="$CWD/packages/$DIRNAME" 31 | 32 | echo "LINT: $DIRNAME ($DIR)" 33 | 34 | cd "$DIR" 35 | 36 | npm run lint 37 | } 38 | 39 | cmd_install() { 40 | run_install plant 41 | run_install http-adapter 42 | run_install http 43 | run_install https 44 | run_install http2 45 | run_install https2 46 | run_install router 47 | run_install flow 48 | } 49 | 50 | cmd_test() { 51 | run_test plant 52 | run_test http-adapter 53 | run_test http 54 | run_test https 55 | run_test http2 56 | run_test https2 57 | run_test router 58 | run_test flow 59 | } 60 | 61 | cmd_lint() { 62 | run_lint plant 63 | run_lint http-adapter 64 | run_lint http 65 | run_lint https 66 | run_lint http2 67 | run_lint https2 68 | run_lint router 69 | run_lint flow 70 | } 71 | 72 | cmd_usage() { 73 | echo "Usage is: test.sh COMMAND" 74 | echo "" 75 | echo "Commands are:" 76 | echo "- install" 77 | echo "- install-eslint" 78 | echo "- lint" 79 | echo "- test" 80 | } 81 | 82 | CMD=$1 83 | shift 1 84 | 85 | case $CMD in 86 | "install") cmd_install $@ ;; 87 | "lint") cmd_lint $@ ;; 88 | "test") cmd_test $@ ;; 89 | *) cmd_usage ;; 90 | esac 91 | -------------------------------------------------------------------------------- /example/context-extension.js: -------------------------------------------------------------------------------- 1 | const Plant = require('@plant/plant') 2 | 3 | const SESSION = Symbol('session') 4 | 5 | // Write session somehow 6 | async function writeSession() {} 7 | // Read session somehow 8 | async function readSession() {} 9 | // Load user somehow 10 | async function loadUser() {} 11 | // Define session web handler 12 | async function sessionHandler(ctx, next) { 13 | const {res} = ctx 14 | // Get session somehow 15 | const session = await readSession(res.cookies.sessionId) 16 | await next({ 17 | ...ctx, 18 | [SESSION]: session, 19 | }) 20 | 21 | await writeSession(session) 22 | } 23 | 24 | // Create server instance 25 | const app = new Plant() 26 | 27 | // Add session to context 28 | app.use(sessionHandler) 29 | 30 | // Use session 31 | app.use(async function(ctx, next) { 32 | const {[SESSION]:session} = ctx 33 | 34 | if (session.userId) { 35 | await next({ 36 | ...ctx, 37 | user: await loadUser(session.userId), 38 | }) 39 | } 40 | else { 41 | await next() 42 | } 43 | }) 44 | -------------------------------------------------------------------------------- /example/context.js: -------------------------------------------------------------------------------- 1 | const createServer = require('@plant/http') 2 | const Plant = require('@plant/plant') 3 | const Router = require('@plant/router') 4 | 5 | const app = new Plant() 6 | 7 | // Add logger to context 8 | app.use(async function(context, next){ 9 | await next({...context, logger: console, user: true}) 10 | }) 11 | 12 | // Add time tracking handler 13 | app.use(async function({logger}, next) { 14 | const start = Date.now() 15 | await next() 16 | logger.info('Logger executed in %s ms', Date.now() - start) 17 | }) 18 | 19 | const router = new Router() 20 | 21 | router.get('/users/:id', async function({req, res}) { 22 | res.text(`User id: ${req.params.id}`) 23 | }) 24 | 25 | router.get('/rooms/:id', async function({req, res}) { 26 | res.text(`Room id: ${req.params.id}`) 27 | }) 28 | 29 | router.route('/places', async function({req, res, version}) { 30 | res.text(`Places(v${version}): ${req.url.pathname}`) 31 | }) 32 | 33 | function contextVersion(version) { 34 | return async function(ctx, next) { 35 | await next({...ctx, version}) 36 | } 37 | } 38 | 39 | app.use('/api/user/', contextVersion(1), router) 40 | app.use('/api/admin/', contextVersion(2), router) 41 | 42 | createServer(app) 43 | .listen(process.env.PORT || 8080) 44 | -------------------------------------------------------------------------------- /example/cookie.js: -------------------------------------------------------------------------------- 1 | const createServer = require('@plant/http') 2 | const Plant = require('@plant/plant') 3 | 4 | const app = new Plant() 5 | 6 | app.use(async function({req, res}){ 7 | res.setCookie('one', 1) 8 | res.setCookie('two', 2) 9 | 10 | res.json(req.cookies) 11 | }) 12 | 13 | createServer(app) 14 | .listen(process.env.PORT || 8080) 15 | -------------------------------------------------------------------------------- /example/custom-transport-basis.js: -------------------------------------------------------------------------------- 1 | const Plant = require('@plant/plant') 2 | 3 | const plant = new Plant() 4 | 5 | const url = new URL('http://localhost:8080/') 6 | 7 | // Create HTTP context's params 8 | const req = new Plant.Request({ 9 | url, 10 | }) 11 | const res = new Plant.Response({ 12 | url, 13 | }) 14 | 15 | // Request peer. Peer represents other side of connection. 16 | const peer = new Plant.Peer({ 17 | uri: new Plant.URI({ 18 | protocol: 'proc:', 19 | hostname: process.pid, 20 | }), 21 | }) 22 | 23 | // Create connection socket 24 | const socket = new Plant.Socket({ 25 | peer, 26 | // If socket allows write upstream, then onPush method could be defined to handle pushes. 27 | // onPush should return Promise which resolves when response sending completes. 28 | // eslint-disable-next-line no-unused-vars 29 | async onPush(response) { 30 | // Send response with web socket 31 | }, 32 | }) 33 | 34 | const handleRequest = plant.getHandler() 35 | 36 | handleRequest({req, res, socket}) 37 | .finally(() => socket.destroy()) 38 | -------------------------------------------------------------------------------- /example/echo.js: -------------------------------------------------------------------------------- 1 | const createServer = require('@plant/http') 2 | const Plant = require('@plant/plant') 3 | 4 | const plant = new Plant() 5 | 6 | plant.use(async ({req, res}) => { 7 | // Set request input stream as response body 8 | res.body = req.body 9 | }) 10 | 11 | createServer(plant.handler()) 12 | .listen(8080) 13 | -------------------------------------------------------------------------------- /example/file-stream.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const createServer = require('@plant/http') 3 | const Plant = require('@plant/plant') 4 | const {ReadableStream} = require('web-streams-polyfill/ponyfill') 5 | 6 | const plant = new Plant() 7 | 8 | plant.use(async ({res}) => { 9 | // Create native Node stream 10 | const stream = fs.createReadStream('./streams.js') 11 | // Wrap Node stream into WebAPI stream 12 | const webStream = wrapNodeStream(stream) 13 | // Send stream with response 14 | res.stream(webStream) 15 | }) 16 | 17 | createServer(plant.handler()) 18 | .listen(8080) 19 | 20 | // Stream wrapper 21 | function wrapNodeStream(stream) { 22 | return new ReadableStream({ 23 | start(controller) { 24 | stream.resume() 25 | stream.on('data', (chunk) => { 26 | controller.enqueue(chunk) 27 | }) 28 | stream.on('end', () => { 29 | controller.close() 30 | }) 31 | }, 32 | cancel() { 33 | stream.close() 34 | }, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /example/hello-world.js: -------------------------------------------------------------------------------- 1 | const createServer = require('@plant/http') 2 | const Plant = require('@plant/plant') 3 | 4 | const plant = new Plant() 5 | 6 | plant.use(async ({res}) => { 7 | // Send text response 8 | res.body = 'Hello, World!' 9 | }) 10 | 11 | createServer(plant.handler()) 12 | .listen(8080) 13 | -------------------------------------------------------------------------------- /example/push.js: -------------------------------------------------------------------------------- 1 | import Plant from '@plant/plant' 2 | import {createServer} from '@plant/http2' 3 | import {serveDir} from '@plant/fs' 4 | 5 | const plant = new Plant() 6 | 7 | plant.use('/assets/*', serveDir('./public')) 8 | 9 | plant.use(({res}) => { 10 | res.push('/assets/index.js') 11 | res.push('/assets/style.css') 12 | 13 | res.html('...') 14 | }) 15 | 16 | createServer(plant) 17 | .listen(8080) 18 | -------------------------------------------------------------------------------- /example/router.js: -------------------------------------------------------------------------------- 1 | const Plant = require('@plant/plant') 2 | const Router = require('@plant/router') 3 | 4 | // Greeting manager is our business logic. It knows nothing about transport 5 | // in our case HTTP. It just do particluar job. 6 | class GreetManager { 7 | constructor(user) { 8 | this.user = user 9 | } 10 | 11 | greet() { 12 | return `Hello, ${this.user}` 13 | } 14 | } 15 | 16 | // Greeting manager router knows how to connect HTTP with businnes logic which 17 | // is a GreetingManager 18 | function greetingRouter(manager) { 19 | const router = new Router() 20 | 21 | router.get('/', ({res}) => { 22 | res.body = manager.greet() 23 | }) 24 | 25 | return router 26 | } 27 | 28 | const plant = new Plant() 29 | 30 | // Now we can add any count of our business logic instances to public HTTP 31 | // interface. 32 | plant.use('/guest', greetingRouter(new GreetManager('guest'))) 33 | plant.use('/admin', greetingRouter(new GreetManager('Admin'))) 34 | plant.use('/world', greetingRouter(new GreetManager('World'))) 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "husky": "^1.2.1" 5 | }, 6 | "husky": { 7 | "hooks": { 8 | "pre-commit": "./bin/lint.sh", 9 | "pre-push": "./bin/test.sh test" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/-/.npmignore: -------------------------------------------------------------------------------- 1 | dev/ 2 | docs/ 3 | tmp/ 4 | local/ 5 | examples/ 6 | test/ 7 | utils/ 8 | 9 | *.pid 10 | *.lock 11 | .jsdoc.json 12 | .testuprc 13 | 14 | # System files 15 | .DS_Store 16 | [Th]umbs.db 17 | Desktop.ini 18 | -------------------------------------------------------------------------------- /packages/electron-adapter/.npmignore: -------------------------------------------------------------------------------- 1 | ../-/.npmignore -------------------------------------------------------------------------------- /packages/electron-adapter/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/electron-adapter", 3 | "version": "0.1.0-alpha1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@plant/flow": { 8 | "version": "1.0.1", 9 | "resolved": "https://registry.npmjs.org/@plant/flow/-/flow-1.0.1.tgz", 10 | "integrity": "sha512-GGN6SEQ12BOIM8ikw0yRyTvHoXRA/upZmPGONO9yZI0g8lv9KykjITgrs9Btyjb/pBlM8mza+F6bFwE4+KvBzA==" 11 | }, 12 | "@plant/node-stream-utils": { 13 | "version": "0.1.2", 14 | "resolved": "https://registry.npmjs.org/@plant/node-stream-utils/-/node-stream-utils-0.1.2.tgz", 15 | "integrity": "sha512-azj7sBxyKRnsDYqVWgEqADMl3Ap79SC9/U9Obtcnq2SvkhhpXy1Jl2nrpArb2J+xK0lOnDC7iAsO5crlcczn+A==" 16 | }, 17 | "@plant/plant": { 18 | "version": "2.4.0", 19 | "resolved": "https://registry.npmjs.org/@plant/plant/-/plant-2.4.0.tgz", 20 | "integrity": "sha512-kNm/cj1txlmd0gZmuc8laztHjDLgYe2qnShjukCQvMbiOA6uz7PM37ZSmSQ0Nc3PY3h4Kv8BJ/mruQt1i9PGKg==", 21 | "requires": { 22 | "@plant/flow": "^1.0.0", 23 | "cookie": "^0.3.1", 24 | "escape-string-regexp": "^2.0.0", 25 | "eventemitter3": "^3.1.0", 26 | "lodash.escaperegexp": "^4.1.2", 27 | "lodash.isobject": "^3.0.2", 28 | "lodash.isplainobject": "^4.0.6", 29 | "lodash.isstring": "^4.0.1", 30 | "statuses": "^1.5.0" 31 | } 32 | }, 33 | "allow-publish-tag": { 34 | "version": "2.1.1", 35 | "resolved": "https://registry.npmjs.org/allow-publish-tag/-/allow-publish-tag-2.1.1.tgz", 36 | "integrity": "sha512-w26dHOZT3Hd70UK/Vukci1wMeces4RzLrmnE+qYinb3cisQU948s/3QoqG38WAs8lW2NwT4MyHeTG4RkZGzwFw==", 37 | "dev": true 38 | }, 39 | "cookie": { 40 | "version": "0.3.1", 41 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 42 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 43 | }, 44 | "escape-string-regexp": { 45 | "version": "2.0.0", 46 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", 47 | "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" 48 | }, 49 | "eventemitter3": { 50 | "version": "3.1.2", 51 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", 52 | "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" 53 | }, 54 | "lodash.escaperegexp": { 55 | "version": "4.1.2", 56 | "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", 57 | "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" 58 | }, 59 | "lodash.isobject": { 60 | "version": "3.0.2", 61 | "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz", 62 | "integrity": "sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0=" 63 | }, 64 | "lodash.isplainobject": { 65 | "version": "4.0.6", 66 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 67 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 68 | }, 69 | "lodash.isstring": { 70 | "version": "4.0.1", 71 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 72 | "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" 73 | }, 74 | "statuses": { 75 | "version": "1.5.0", 76 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 77 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/electron-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/electron-adapter", 3 | "version": "0.1.0", 4 | "description": "Plant server adapter for Electron", 5 | "main": "src/index.js", 6 | "engines": { 7 | "node": ">=8.0" 8 | }, 9 | "scripts": { 10 | "lint": "npm run lint:src && npm run lint:test", 11 | "lint:src": "eslint src/**.js", 12 | "lint:test": "eslint test/**.js", 13 | "prepublishOnly": "allow-publish-tag next && npm run lint && npm test", 14 | "test": "echo 'No tests yet'" 15 | }, 16 | "license": "MIT", 17 | "dependencies": { 18 | "@plant/node-stream-utils": "^0.1.2" 19 | }, 20 | "devDependencies": { 21 | "allow-publish-tag": "^2.1.1" 22 | }, 23 | "peerDependencies": { 24 | "@plant/plant": "^2.0.0" 25 | }, 26 | "publishConfig": { 27 | "access": "public" 28 | }, 29 | "directories": { 30 | "test": "test" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/rumkin/plant.git" 35 | }, 36 | "keywords": [ 37 | "@plant/plant", 38 | "web", 39 | "http", 40 | "handler" 41 | ], 42 | "author": "rumkin", 43 | "homepage": "https://github.com/rumkin/plant/tree/master/packages/electron-adapter", 44 | "bugs": "https://github.com/rumkin/plant/issues" 45 | } 46 | -------------------------------------------------------------------------------- /packages/electron-adapter/readme.md: -------------------------------------------------------------------------------- 1 | # Electron 2 | 3 | Adapter to make Plant work in Electron, using Electrons streaming API. 4 | 5 | ## Install 6 | 7 | ``` 8 | npm i @plant/electron-adapter 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | const {protocol, session} = require('electron') 15 | const Plant = require('@plant/plant') 16 | const {createRequestHandler} = require('@plant/electron-adapter') 17 | 18 | const plant = new Plant() 19 | 20 | plant.use(({res}) => { 21 | res.json({ok: true}) 22 | }) 23 | 24 | // Serve standard protocol 25 | protocol.interceptStreamProtocol('https', createRequestHandler(plant, session)) 26 | // Serve custom protocol 27 | protocol.registerStreamProtocol('plant', createRequestHandler(plant, session)) 28 | ``` 29 | 30 | ## License 31 | 32 | MIT © [Rumkin](https://rumk.in) 33 | -------------------------------------------------------------------------------- /packages/electron-adapter/src/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const {Readable, PassThrough} = require('stream') 3 | 4 | const Plant = require('@plant/plant') 5 | const {WebToNodeStream, NodeToWebStream} = require('@plant/node-stream-utils') 6 | 7 | const {Request, Response, Socket, Peer, URI} = Plant 8 | 9 | function withCallback(promise, callback) { 10 | if (callback) { 11 | return promise.then(callback) 12 | } 13 | else { 14 | return promise 15 | } 16 | } 17 | 18 | function createRequestHandler(plant, session) { 19 | const handle = plant.getHandler() 20 | 21 | return function(request, callback) { 22 | return withCallback( 23 | handleRequest(handle, session, request), 24 | callback, 25 | ) 26 | } 27 | } 28 | 29 | async function handleRequest(handle, session, request) { 30 | const req = new Request({ 31 | method: request.method, 32 | url: new URL(request.url), 33 | body: createBodyFromUploadData(session, request.uploadData), 34 | }) 35 | const res = new Response({ 36 | url: new URL(request.url), 37 | }) 38 | 39 | await handle({ 40 | req, 41 | res, 42 | socket: new Socket({ 43 | peer: new Peer({ 44 | uri: new URI({ 45 | protocol: 'electron', 46 | hostname: process.pid, 47 | }), 48 | }), 49 | }), 50 | }) 51 | 52 | let data 53 | if (res.hasBody === false) { 54 | throw new Error('Response is empty') 55 | } 56 | 57 | if (typeof res.body === 'string') { 58 | const stream = new PassThrough() 59 | 60 | stream.pause() 61 | stream.write(res.body) 62 | stream.end() 63 | 64 | data = stream 65 | } 66 | // Wrapped Stream 67 | else if (res.body.stream) { 68 | data = res.body.stream 69 | } 70 | else { 71 | data = new WebToNodeStream(res.body) 72 | } 73 | 74 | const headers = {} 75 | for (const name of res.headers.keys()) { 76 | const value = res.headers.raw(name) 77 | if (value.length === 1) { 78 | headers[name] = value[0] 79 | } 80 | else { 81 | headers[name] = value 82 | } 83 | } 84 | 85 | return { 86 | statusCode: res.status, 87 | headers, 88 | data, 89 | } 90 | } 91 | 92 | function createBodyFromUploadData(session, uploadData = []) { 93 | if (! uploadData.length) { 94 | return null 95 | } 96 | 97 | const items = [...uploadData] 98 | 99 | const generator = async function * () { 100 | while (items.length) { 101 | const item = items.shift() 102 | 103 | if (item.bytes) { 104 | yield item.bytes 105 | } 106 | else if (item.blobUUID) { 107 | yield await session.getBlobData(item.blobUUID) 108 | } 109 | else { 110 | const file = fs.createReadStream(item.file) 111 | for await (const chunk of file) { 112 | yield chunk 113 | } 114 | } 115 | } 116 | } 117 | 118 | const stream = Readable.from(generator()) 119 | stream.pause() 120 | 121 | return new NodeToWebStream(stream) 122 | } 123 | 124 | exports.createRequestHandler = createRequestHandler 125 | -------------------------------------------------------------------------------- /packages/electron-adapter/test/index.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumkin/plant/061f376dbdee578ce141abec39a5bc06c6a86f97/packages/electron-adapter/test/index.spec.js -------------------------------------------------------------------------------- /packages/electron/.npmignore: -------------------------------------------------------------------------------- 1 | ../-/.npmignore -------------------------------------------------------------------------------- /packages/electron/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/electron", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "allow-publish-tag": { 8 | "version": "2.1.1", 9 | "resolved": "https://registry.npmjs.org/allow-publish-tag/-/allow-publish-tag-2.1.1.tgz", 10 | "integrity": "sha512-w26dHOZT3Hd70UK/Vukci1wMeces4RzLrmnE+qYinb3cisQU948s/3QoqG38WAs8lW2NwT4MyHeTG4RkZGzwFw==", 11 | "dev": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/electron", 3 | "version": "0.1.0", 4 | "description": "Listen Electron's protocols", 5 | "main": "src/index.js", 6 | "engines": { 7 | "electron": ">=7.0", 8 | "node": ">=8.0" 9 | }, 10 | "scripts": { 11 | "lint": "npm run lint:src && npm run lint:test", 12 | "lint:src": "eslint src/**.js", 13 | "lint:test": "eslint test/**.js", 14 | "prepublishOnly": "allow-publish-tag next && npm run lint && npm test", 15 | "test": "echo 'No tests yet'" 16 | }, 17 | "license": "MIT", 18 | "dependencies": { 19 | "@plant/electron-adapter": "^0.1.0" 20 | }, 21 | "devDependencies": { 22 | "allow-publish-tag": "^2.1.1" 23 | }, 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "directories": { 28 | "test": "test" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/rumkin/plant.git" 33 | }, 34 | "keywords": [ 35 | "@plant/plant", 36 | "web", 37 | "http", 38 | "handler" 39 | ], 40 | "author": "rumkin", 41 | "homepage": "https://github.com/rumkin/plant/tree/master/packages/electron", 42 | "bugs": "https://github.com/rumkin/plant/issues" 43 | } 44 | -------------------------------------------------------------------------------- /packages/electron/readme.md: -------------------------------------------------------------------------------- 1 | # Electron 2 | 3 | Helpers to make plant work in Electron, using Electrons streaming API. 4 | 5 | ## Install 6 | 7 | ``` 8 | npm i @plant/electron 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | const Plant = require('@plant/plant') 15 | const {createServer} = require('@plant/electron') 16 | 17 | const plant = new Plant() 18 | 19 | plant.use(({res}) => { 20 | res.json({ok: true}) 21 | }) 22 | 23 | createServer(plant) 24 | // Serve requests via https:// protocol 25 | .interceptProtocol('https') 26 | // Serve requests via my:// protocol (made up). 27 | .registerProtocol('my') 28 | ``` 29 | 30 | ## License 31 | 32 | MIT © [Rumkin](https://rumk.in) 33 | -------------------------------------------------------------------------------- /packages/electron/src/index.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron') 2 | const {createRequestHandler} = require('@plant/electron-adapter') 3 | 4 | const {protocol} = electron 5 | 6 | function createServer(plant, { 7 | session = electron.session, 8 | } = {}) { 9 | return new ElectronServer({ 10 | plant, 11 | session, 12 | }) 13 | } 14 | 15 | class ElectronServer { 16 | constructor({plant, session}) { 17 | this.plant = plant 18 | this.session = session 19 | } 20 | 21 | getHandler() { 22 | const handle = createRequestHandler( 23 | this.plant, this.session 24 | ) 25 | 26 | return (request, callback) => { 27 | handle(request) 28 | .then(callback) 29 | } 30 | } 31 | 32 | interceptProtocol(name) { 33 | protocol.interceptStreamProtocol( 34 | name, this.getHandler() 35 | ) 36 | return this 37 | } 38 | 39 | registerProtocol(name) { 40 | protocol.registerStreamProtocol( 41 | name, this.getHandler() 42 | ) 43 | 44 | return this 45 | } 46 | } 47 | 48 | module.exports = createServer 49 | -------------------------------------------------------------------------------- /packages/electron/test/index.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumkin/plant/061f376dbdee578ce141abec39a5bc06c6a86f97/packages/electron/test/index.spec.js -------------------------------------------------------------------------------- /packages/flow/.npmignore: -------------------------------------------------------------------------------- 1 | ../-/.npmignore -------------------------------------------------------------------------------- /packages/flow/.testuprc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": "console" 3 | } 4 | -------------------------------------------------------------------------------- /packages/flow/license: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Copyright (c) 2017-2019 Rumkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/flow/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/flow", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@testup/cli": { 8 | "version": "0.3.0", 9 | "resolved": "https://registry.npmjs.org/@testup/cli/-/cli-0.3.0.tgz", 10 | "integrity": "sha512-STQyGlzWgeOjq6hGJ8V9AoI/pK84ZjVlb/LThsrmTD36cK6fZKf+rqWwkPfftypDzXx4UcWmb2yG/AObJ404MA==", 11 | "requires": { 12 | "coa": "^2.0.2" 13 | } 14 | }, 15 | "@testup/console-reporter": { 16 | "version": "0.1.1", 17 | "resolved": "https://registry.npmjs.org/@testup/console-reporter/-/console-reporter-0.1.1.tgz", 18 | "integrity": "sha512-+mnpuzbzMJSlNcHtuJthDBJSa1tsWjXTeGwRsRo3QNkNvkp5NOdJxqUNT1SPFjdFHcAe7mfO2TXO4QWYnTkrDQ==" 19 | }, 20 | "@testup/core": { 21 | "version": "0.1.2", 22 | "resolved": "https://registry.npmjs.org/@testup/core/-/core-0.1.2.tgz", 23 | "integrity": "sha512-/IfKbu399VoqCQY9suEbU18+3Uxyn/14uVrg8MBt+NO+3EnyOtd/ZPCt2mMWGcvPiXcT8W1QhssSg53gd2x0WQ==" 24 | }, 25 | "@types/q": { 26 | "version": "1.5.2", 27 | "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", 28 | "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==" 29 | }, 30 | "allow-publish-tag": { 31 | "version": "1.0.1", 32 | "resolved": "https://registry.npmjs.org/allow-publish-tag/-/allow-publish-tag-1.0.1.tgz", 33 | "integrity": "sha512-KBTWMHR8AVqQ+zpTH3WqMPb62uz+ZiE7xhiAK/CZyuYxW+ifNSv4zcskfP91wsY3al33665BeYOkNhUynCmctA==", 34 | "dev": true 35 | }, 36 | "ansi-styles": { 37 | "version": "3.2.1", 38 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 39 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 40 | "requires": { 41 | "color-convert": "^1.9.0" 42 | } 43 | }, 44 | "chalk": { 45 | "version": "2.4.2", 46 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 47 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 48 | "requires": { 49 | "ansi-styles": "^3.2.1", 50 | "escape-string-regexp": "^1.0.5", 51 | "supports-color": "^5.3.0" 52 | } 53 | }, 54 | "coa": { 55 | "version": "2.0.2", 56 | "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", 57 | "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", 58 | "requires": { 59 | "@types/q": "^1.5.1", 60 | "chalk": "^2.4.1", 61 | "q": "^1.1.2" 62 | } 63 | }, 64 | "color-convert": { 65 | "version": "1.9.3", 66 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 67 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 68 | "requires": { 69 | "color-name": "1.1.3" 70 | } 71 | }, 72 | "color-name": { 73 | "version": "1.1.3", 74 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 75 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 76 | }, 77 | "escape-string-regexp": { 78 | "version": "1.0.5", 79 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 80 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 81 | }, 82 | "has-flag": { 83 | "version": "3.0.0", 84 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 85 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 86 | }, 87 | "q": { 88 | "version": "1.5.1", 89 | "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", 90 | "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" 91 | }, 92 | "supports-color": { 93 | "version": "5.5.0", 94 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 95 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 96 | "requires": { 97 | "has-flag": "^3.0.0" 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /packages/flow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/flow", 3 | "version": "1.0.2", 4 | "description": "Async flow library for Plant server", 5 | "repository": { 6 | "type": "git", 7 | "url": "github.com/rumkin/plant" 8 | }, 9 | "homepage": "https://github.com/rumkin/plant/tree/master/packages/router", 10 | "author": "Rumkin (https://rumk.in/)", 11 | "main": "./src/index.js", 12 | "engines": { 13 | "node": ">=6" 14 | }, 15 | "devDependencies": { 16 | "@testup/cli": "^0.3.0", 17 | "@testup/console-reporter": "^0.1.1", 18 | "@testup/core": "^0.1.2", 19 | "allow-publish-tag": "^1.0.1" 20 | }, 21 | "scripts": { 22 | "lint": "npm run lint:src && npm run lint:test", 23 | "lint:src": "eslint src/**.js", 24 | "lint:test": "eslint test/**.js", 25 | "prepublishOnly": "npm test", 26 | "test": "testup run test/**.spec.js" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/flow/readme.md: -------------------------------------------------------------------------------- 1 | # Flow 2 | 3 | Methods to control cascades flow: 4 | 5 | * cascade 6 | * whileLoop 7 | * and 8 | * or 9 | * getHandler 10 | -------------------------------------------------------------------------------- /packages/flow/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Plant.Flow 3 | * @description Methods to control cascade flows. 4 | */ 5 | 6 | function noop() {} 7 | 8 | /** 9 | * Create async cascade resolver. 10 | * @param {...HandleType} args Handlable async functions. 11 | * @returns {function(Context)} Returns function which pass context through the stack. 12 | */ 13 | function cascade(...args) { 14 | async function passThrough(initialCtx) { 15 | let ctx = initialCtx 16 | 17 | function next(handlers, newCtx) { 18 | ctx = newCtx || ctx 19 | 20 | if (handlers.length) { 21 | return handlers[0](Object.assign({}, ctx), next.bind(null, handlers.slice(1))) 22 | } 23 | } 24 | 25 | return await next(args, initialCtx) 26 | } 27 | 28 | return passThrough 29 | } 30 | 31 | /** 32 | * Creates async queue resolver which works while condition returns false. 33 | * 34 | * @param {function(Context)} condition - Condition function which returns bool. 35 | * @returns {function(...HandleType)} Handlable async queue handler creator. 36 | */ 37 | function whileLoop(condition) { 38 | return function(...handlers) { 39 | return conditional.bind(null, handlers, condition) 40 | } 41 | } 42 | 43 | async function conditional(handlers, condition, ctx, next) { 44 | for (const handler of handlers) { 45 | await handler(Object.assign({}, ctx), noop) 46 | 47 | if (condition(ctx) === false) { 48 | return 49 | } 50 | } 51 | 52 | await next() 53 | } 54 | 55 | /** 56 | * Handlable object should countain method handler() which returns async 57 | * function. This function receive two params: context and next. 58 | * @type {Handlable} 59 | * @prop {function()} handler Return async function. 60 | */ 61 | 62 | /** 63 | * Get function from passed value. 64 | * @param {function|Handlable} handler Handlable value. 65 | * @return {function(object,function)} Returns function. 66 | */ 67 | function getHandler(handler) { 68 | if (typeof handler === 'object') { 69 | return handler.getHandler() 70 | } 71 | else { 72 | return handler 73 | } 74 | } 75 | 76 | /** 77 | * Determine that request is finished. Using to manage cascade depth. 78 | * 79 | * @param {NativeContext} options Native context. 80 | * @returns {Boolean} Return true if response has body or socket closed. 81 | */ 82 | function isNotFinished({res, socket}) { 83 | return res.hasBody === false && socket.isEnded === false 84 | } 85 | 86 | /** 87 | * Create async request handlers queue. It iterate request handlers and if 88 | * request handler doesn't sent response it runs next request handler and so. 89 | * 90 | * @param {...(function()|Handlable)} handlers Handlable async functions. 91 | * @return {function(object,function)} Returns function which pass context through the queue. 92 | */ 93 | const whileNotFinished = whileLoop(isNotFinished) 94 | 95 | /** 96 | * Returns function that runs handlers until request headers are not sent. 97 | * 98 | * @param {...(function()|Handlable)} args - List of handlable values. 99 | * @returns {function(object, function())} Returns function to pass value into handlers. 100 | */ 101 | const or = function(...args) { 102 | return whileNotFinished(...args.map(getHandler)) 103 | } 104 | 105 | /** 106 | * Returns function that runs handlers in depth. 107 | * 108 | * @param {...(function()|Handlable)} args - List of handlable values. 109 | * @returns {function(object)} Returns function to pass value into handlers. 110 | */ 111 | const and = function(...args) { 112 | return cascade(...args.map(getHandler)) 113 | } 114 | 115 | exports.cascade = cascade 116 | exports.whileLoop = whileLoop 117 | exports.or = or 118 | exports.and = and 119 | exports.getHandler = getHandler 120 | -------------------------------------------------------------------------------- /packages/flow/test/flow.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | const Server = require('..') 4 | const {and, or, getHandler} = Server 5 | 6 | module.exports = ({describe, it}) => describe('Plant.Flow', function() { 7 | describe('Cascade', function() { 8 | describe('and()', () => { 9 | it ('should iterate over `and`', async function() { 10 | let round = 0 11 | 12 | const fn = and( 13 | async function(ctx, next) { 14 | round += 1 15 | assert.equal(round, 1) 16 | 17 | await next() 18 | 19 | assert.equal(round, 3) 20 | }, 21 | async function(ctx, next) { 22 | round += 1 23 | assert.equal(round, 2) 24 | 25 | await next() 26 | // eslint-disable-next-line require-atomic-updates 27 | round += 1 28 | } 29 | ) 30 | 31 | await fn(null, null) 32 | 33 | assert.equal(round, 3) 34 | }) 35 | }) 36 | 37 | describe('or()', () => { 38 | it('Should stop when res.hasBody is `true`', async () => { 39 | const res = { 40 | hasBody: false, 41 | } 42 | const socket = { 43 | isEnded: false, 44 | } 45 | 46 | let i = 0 47 | 48 | await or( 49 | async function(ctx) { 50 | ctx.res.hasBody = true 51 | i++ 52 | }, 53 | async function() { 54 | i++ 55 | }, 56 | )({res, socket}) 57 | 58 | assert.equal(i, 1) 59 | }) 60 | 61 | it('Should stop when socket.isEnded is `true`', async () => { 62 | const res = { 63 | hasBody: false, 64 | } 65 | const socket = { 66 | isEnded: false, 67 | } 68 | 69 | let i = 0 70 | 71 | await or( 72 | async function(ctx) { 73 | ctx.socket.isEnded = true 74 | i++ 75 | }, 76 | async function() { 77 | i++ 78 | }, 79 | )({res, socket}) 80 | 81 | assert.equal(i, 1) 82 | }) 83 | 84 | it('Should iterate till the end', async () => { 85 | const res = { 86 | hasBody: false, 87 | } 88 | const socket = { 89 | isEnded: false, 90 | } 91 | 92 | let i = 0 93 | 94 | await or( 95 | async function() { 96 | i++ 97 | }, 98 | async function(ctx) { 99 | i++ 100 | ctx.res.hasBody = true 101 | }, 102 | )({res, socket}) 103 | 104 | assert.equal(i, 2) 105 | }) 106 | 107 | it('Should continue iteration if no matches found', async () => { 108 | const res = { 109 | hasBody: false, 110 | } 111 | const socket = { 112 | isEnded: false, 113 | } 114 | 115 | let i = 0 116 | 117 | await or( 118 | async function() { 119 | i++ 120 | }, 121 | async function() { 122 | i++ 123 | }, 124 | )({res, socket}, () => { 125 | i++ 126 | }) 127 | 128 | assert.equal(i, 3) 129 | }) 130 | }) 131 | 132 | describe('getHandler()', () => { 133 | it('Should call .getHandler() of object', () => { 134 | let i = 0 135 | 136 | getHandler({ 137 | getHandler() { 138 | i++ 139 | return () => {} 140 | }, 141 | }) 142 | 143 | assert(i, 1) 144 | }) 145 | }) 146 | }) 147 | }) 148 | -------------------------------------------------------------------------------- /packages/fs/.npmignore: -------------------------------------------------------------------------------- 1 | ../-/.npmignore -------------------------------------------------------------------------------- /packages/fs/license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Rumkin (rumk.in) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/fs/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/fs", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@plant/vfs": { 8 | "version": "0.1.0-a.1", 9 | "resolved": "https://registry.npmjs.org/@plant/vfs/-/vfs-0.1.0-a.1.tgz", 10 | "integrity": "sha512-pRVdlM/IFKH9NfyOizaz0lKFpw9w7AYXbmPrm4oGI62Qbq3hcpf4tay3aVc3O0OPQeArpdXYybbHZEMTvxaUjg==", 11 | "requires": { 12 | "escape-html": "^1.0.3", 13 | "mime": "^2.4.4" 14 | } 15 | }, 16 | "allow-publish-tag": { 17 | "version": "2.1.1", 18 | "resolved": "https://registry.npmjs.org/allow-publish-tag/-/allow-publish-tag-2.1.1.tgz", 19 | "integrity": "sha512-w26dHOZT3Hd70UK/Vukci1wMeces4RzLrmnE+qYinb3cisQU948s/3QoqG38WAs8lW2NwT4MyHeTG4RkZGzwFw==", 20 | "dev": true 21 | }, 22 | "escape-html": { 23 | "version": "1.0.3", 24 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 25 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 26 | }, 27 | "mime": { 28 | "version": "2.4.4", 29 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", 30 | "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" 31 | }, 32 | "web-streams-polyfill": { 33 | "version": "2.0.3", 34 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-2.0.3.tgz", 35 | "integrity": "sha512-pOqiHmL3RBAGS+SgOR42RbPU6nc8/n15N2rsOXFYHLnTfs2Z8QHs8AizOeOaYEnhwPN4+hu3M2D9XvAqzvt6MA==" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/fs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/fs", 3 | "version": "0.1.1", 4 | "description": "Node.js FS module bindings for VFS handlers", 5 | "main": "src/index.js", 6 | "engines": { 7 | "node": ">=8.0" 8 | }, 9 | "scripts": { 10 | "lint": "npm run lint:src && npm run lint:test", 11 | "lint:src": "eslint src/**.js", 12 | "lint:test": "eslint test/**.js", 13 | "prepublishOnly": "allow-publish-tag next && npm run lint", 14 | "test": "echo 'No test' && exit 1" 15 | }, 16 | "license": "MIT", 17 | "devDependencies": { 18 | "allow-publish-tag": "^2.1.1" 19 | }, 20 | "dependencies": { 21 | "web-streams-polyfill": "^2.0.3", 22 | "@plant/vfs": "^0.1.0" 23 | }, 24 | "peerDependencies": {}, 25 | "publishConfig": { 26 | "access": "public" 27 | }, 28 | "directories": { 29 | "test": "test" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/rumkin/plant.git" 34 | }, 35 | "keywords": [ 36 | "@plant/plant", 37 | "web", 38 | "http", 39 | "handler" 40 | ], 41 | "author": "rumkin", 42 | "homepage": "https://github.com/rumkin/plant/tree/master/packages/fs", 43 | "bugs": "https://github.com/rumkin/plant/issues" 44 | } 45 | -------------------------------------------------------------------------------- /packages/fs/readme.md: -------------------------------------------------------------------------------- 1 | # FS 2 | 3 | Is web handler factories for Plant's VFS module bound with node.js fs module. 4 | 5 | ## License 6 | 7 | MIT © [Rumkin](https://rumk.in) 8 | -------------------------------------------------------------------------------- /packages/fs/src/custom-fs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const {promisify} = require('util') 3 | const {ReadableStream} = require('web-streams-polyfill/ponyfill') 4 | 5 | module.exports = { 6 | exists: promisify(fs.exists), 7 | stat: promisify(fs.stat), 8 | lstat: promisify(fs.lstat), 9 | readDir: promisify(fs.readdir), 10 | readLink: promisify(fs.readlink), 11 | writeFile: promisify(fs.writeFile), 12 | readFile: promisify(fs.readFile), 13 | createReadStream(filepath, options) { 14 | let stream 15 | let isCanceled = false 16 | 17 | return new ReadableStream({ 18 | start(controller) { 19 | stream = fs.createReadStream(filepath, options) 20 | 21 | stream.on('data', (chunk) => { 22 | controller.enqueue(chunk) 23 | }) 24 | stream.on('end', () => { 25 | if (isCanceled) { 26 | return 27 | } 28 | 29 | isCanceled = true 30 | controller.close() 31 | }) 32 | stream.on('error', (error) => { 33 | if (isCanceled) { 34 | return 35 | } 36 | 37 | isCanceled = true 38 | controller.error(error) 39 | }) 40 | stream.on('close', () => { 41 | if (isCanceled) { 42 | return 43 | } 44 | 45 | isCanceled = true 46 | controller.close() 47 | }) 48 | }, 49 | cancel() { 50 | if (isCanceled) { 51 | return 52 | } 53 | 54 | if (stream) { 55 | stream.destroy() 56 | } 57 | 58 | isCanceled = true 59 | }, 60 | }) 61 | }, 62 | } 63 | -------------------------------------------------------------------------------- /packages/fs/src/index.js: -------------------------------------------------------------------------------- 1 | const vfs = require('@plant/vfs') 2 | const customFs = require('./custom-fs') 3 | 4 | function serveDir(...args) { 5 | return vfs.createDirHandler(customFs, ...args) 6 | } 7 | 8 | function serveFile(...args) { 9 | return vfs.createFileHandler(customFs, ...args) 10 | } 11 | 12 | exports.serveDir = serveDir 13 | exports.serveFile = serveFile 14 | -------------------------------------------------------------------------------- /packages/fs/test/index.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumkin/plant/061f376dbdee578ce141abec39a5bc06c6a86f97/packages/fs/test/index.spec.js -------------------------------------------------------------------------------- /packages/http-adapter/.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": ["jsdoc"] 5 | }, 6 | "source": { 7 | "include": ["src", "package.json", "README.md"], 8 | "includePattern": ".js$", 9 | "excludePattern": "(node_modules/|test/|docs)" 10 | }, 11 | "plugins": [ 12 | "plugins/markdown" 13 | ], 14 | "templates": { 15 | "cleverLinks": false, 16 | "monospaceLinks": true, 17 | "useLongnameInNav": false, 18 | "showInheritedInNav": true 19 | }, 20 | "opts": { 21 | "destination": "./tmp/docs/", 22 | "encoding": "utf8", 23 | "private": true, 24 | "recurse": true, 25 | "template": "./node_modules/minami" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/http-adapter/.npmignore: -------------------------------------------------------------------------------- 1 | ../-/.npmignore -------------------------------------------------------------------------------- /packages/http-adapter/LICENSE: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Copyright (c) 2017-2018 Rumkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/http-adapter/changelog.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | 4 | ### v1.0 5 | 6 | - Http adapter allows intermediate handlers which has access to http request 7 | and response instances and Plant's request and response objects 8 | simultaneously. 9 | - Http adapter wraps request into ReadableStreams. 10 | - Http adapter 11 | 12 | ### v0.1.0 13 | 14 | - Implement handler factory method. 15 | -------------------------------------------------------------------------------- /packages/http-adapter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/http-adapter", 3 | "version": "1.4.0", 4 | "description": "Plant web server node.js HTTP adapter", 5 | "main": "src/index.js", 6 | "engines": { 7 | "node": ">=10" 8 | }, 9 | "scripts": { 10 | "test": "mocha test/index.js", 11 | "lint": "npm run lint:src && npm run lint:test", 12 | "lint:src": "eslint src/**.js", 13 | "lint:test": "eslint test/**.js", 14 | "prepublishOnly": "allow-publish-tag next && npm run lint && npm docs", 15 | "jsdoc": "jsdoc --configure .jsdoc.json --verbose", 16 | "docs": "npm run jsdoc && rm -rf docs && mv tmp/docs/*/*/* docs && rm -rf tmp/docs" 17 | }, 18 | "license": "MIT", 19 | "devDependencies": { 20 | "@plant/plant": "^2.0.0", 21 | "@plant/test-http": "^0.5.3", 22 | "allow-publish-tag": "^2.0.0", 23 | "istanbul": "^0.4.5", 24 | "jsdoc": "^3.6.3", 25 | "minami": "^1.2.3", 26 | "mocha": "^5.2.0", 27 | "node-fetch": "^2.0.0", 28 | "should": "^11.2.1" 29 | }, 30 | "dependencies": { 31 | "lodash.isobject": "^3.0.2" 32 | }, 33 | "peerDependencies": { 34 | "@plant/flow": "^1.0.0" 35 | }, 36 | "publishConfig": { 37 | "access": "public" 38 | }, 39 | "directories": { 40 | "test": "test" 41 | }, 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/rumkin/plant.git" 45 | }, 46 | "keywords": [ 47 | "@plant/plant", 48 | "web", 49 | "http", 50 | "handler" 51 | ], 52 | "author": "rumkin", 53 | "homepage": "https://github.com/rumkin/plant", 54 | "bugs": "https://github.com/rumkin/plant/issues" 55 | } 56 | -------------------------------------------------------------------------------- /packages/http-adapter/readme.md: -------------------------------------------------------------------------------- 1 | # Plant HTTP Adapter 2 | 3 | --- 4 | 5 | This handler connect Plant and native Node's HTTP server. 6 | 7 | ## Install 8 | 9 | Production version from NPM registry: 10 | 11 | ```bash 12 | npm i @plant/http 13 | ``` 14 | 15 | Adapter has Plant as peer dependency so you need to install it too: 16 | 17 | ```bash 18 | npm i @plant/plant 19 | ``` 20 | 21 | 22 | ## Usage 23 | 24 | ```javascript 25 | const http = require('http'); 26 | const Plant = require('@plant/plant'); 27 | const {createRequestHandler} = require('@plant/http-adapter'); 28 | 29 | const plant = new Plant(); 30 | 31 | // Send text response 32 | plant.use('/greet', async function({res}) { 33 | res.body = 'Hello World'; 34 | }); 35 | 36 | // Build request handler 37 | http.createServer(createRequestHandler(plant)) 38 | .listen(80); 39 | ``` 40 | 41 | ## Examples 42 | 43 | * [Hello World](https://github.com/rumkin/plant/tree/master/example/hello-world.js). 44 | 45 | ## API 46 | 47 | ### `createRequestHandler()` 48 | ```text 49 | (plant:Plant, intermediate: Handler) -> HttpRequestListener 50 | ``` 51 | 52 | Return native HTTP server request handler, which pass request through 53 | `intermediate` handlers and then pass to the `plant`. Intermediate handlers 54 | have access to both HTTP (`httpReq`, `httpRes`) and Plant (`req`, `res`) 55 | requests and responses. 56 | 57 | ### `HttpRequestListener()` 58 | ```text 59 | (req: http.Request, res: http.Response) -> void 60 | ``` 61 | 62 | This is standard request listener from Node.js HTTP built-in module. 63 | 64 | 65 | ## License 66 | 67 | MIT © [Rumkin](https://rumk.in) 68 | -------------------------------------------------------------------------------- /packages/http-adapter/test/http.spec.js: -------------------------------------------------------------------------------- 1 | const createHttpServer = require('@plant/test-http/http') 2 | const createTest = require('./test-suite') 3 | 4 | const {createRequestHandler} = require('..') 5 | 6 | function createServer(plant, options) { 7 | return createHttpServer(createRequestHandler(plant, options), options) 8 | } 9 | 10 | createTest('HTTP', createServer) 11 | -------------------------------------------------------------------------------- /packages/http-adapter/test/http2.spec.js: -------------------------------------------------------------------------------- 1 | const createHttpServer = require('@plant/test-http/http2') 2 | const createTest = require('./test-suite') 3 | 4 | const {createRequestHandler} = require('..') 5 | 6 | function createServer(plant, options) { 7 | return createHttpServer(createRequestHandler(plant, options), options) 8 | } 9 | 10 | createTest('HTTP2', createServer) 11 | -------------------------------------------------------------------------------- /packages/http-adapter/test/index.js: -------------------------------------------------------------------------------- 1 | require('./http.spec') 2 | require('./http2.spec') 3 | -------------------------------------------------------------------------------- /packages/http/.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": ["jsdoc"] 5 | }, 6 | "source": { 7 | "include": [".", "package.json", "README.md"], 8 | "includePattern": ".js$", 9 | "excludePattern": "(node_modules/|test/|docs/|.spec.js$)" 10 | }, 11 | "plugins": [ 12 | "plugins/markdown" 13 | ], 14 | "templates": { 15 | "cleverLinks": false, 16 | "monospaceLinks": true, 17 | "useLongnameInNav": false, 18 | "showInheritedInNav": true 19 | }, 20 | "opts": { 21 | "destination": "./tmp/docs/", 22 | "encoding": "utf8", 23 | "private": true, 24 | "recurse": true, 25 | "template": "./node_modules/minami" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/http/.npmignore: -------------------------------------------------------------------------------- 1 | ../-/.npmignore -------------------------------------------------------------------------------- /packages/http/LICENSE: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Copyright (c) 2017-2018 Rumkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/http", 3 | "version": "1.0.4", 4 | "description": "Plant node.js HTTP module's server handler", 5 | "main": "src/index.js", 6 | "engines": { 7 | "node": ">=6" 8 | }, 9 | "scripts": { 10 | "lint": "npm run lint:src && npm run lint:test", 11 | "lint:src": "eslint src/**.js", 12 | "lint:test": "eslint test/**.js", 13 | "test": "mocha test/*.spec.js", 14 | "jsdoc": "jsdoc --configure .jsdoc.json --verbose", 15 | "docs": "npm run jsdoc && rm -rf docs && mv tmp/docs/*/*/* docs && rm -rf tmp/docs", 16 | "prepublishOnly": "allow-publish-tag next && npm run lint && npm test" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "github.com/rumkin/plant" 21 | }, 22 | "keywords": [ 23 | "plant", 24 | "http", 25 | "native-http" 26 | ], 27 | "author": "Rumkin (https://rumk.in/)", 28 | "license": "MIT", 29 | "dependencies": { 30 | "@plant/http-adapter": "^1.4.0" 31 | }, 32 | "devDependencies": { 33 | "@plant/plant": "^2.0.0", 34 | "@plant/test-http": "^0.5.3", 35 | "allow-publish-tag": "^2.0.0", 36 | "jsdoc": "^3.6.3", 37 | "minami": "^1.2.3", 38 | "mocha": "^5.2.0", 39 | "should": "^13.2.2" 40 | }, 41 | "publishConfig": { 42 | "access": "public" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/http/readme.md: -------------------------------------------------------------------------------- 1 | # Node HTTP for Plant 2 | 3 | Plant adapter for native node.js http module. It creates server listener from plant instance. 4 | 5 | ## Install 6 | 7 | ```bash 8 | npm i @plant/http 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```javascript 14 | const Plant = require('@plant/plant'); 15 | const {createServer} = require('@plant/http'); 16 | 17 | const plant = new Plant(); 18 | 19 | plant.use(({res}) => { 20 | res.body = 'Ok'; 21 | }); 22 | 23 | createServer(plant).listen(80); 24 | ``` 25 | 26 | ## License 27 | 28 | MIT © [Rumkin](https://rumk.in) 29 | -------------------------------------------------------------------------------- /packages/http/src/index.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const {createRequestHandler} = require('@plant/http-adapter') 3 | 4 | /** 5 | * createServer - creates http server instance with Plant as request handler. 6 | * 7 | * @param {Plant} plant Plant instance. 8 | * @param {object} options HTTP createServer method options. 9 | * @return {net.Server} Http server instance ready to listen port. 10 | * @example 11 | * 12 | * const createServer = require('@plant/http') 13 | * const Plant = require('@plant/plant') 14 | * 15 | * const plant = new Plant() 16 | * 17 | * plant.use(async ({res}) => { 18 | * res.body = 'Hello, World!' 19 | * }) 20 | * 21 | * createServer(plant).listen(8080) 22 | */ 23 | function createServer(plant, options = {}) { 24 | const server = http.createServer( 25 | options, createRequestHandler(plant) 26 | ) 27 | 28 | return server 29 | } 30 | 31 | exports.createServer = createServer 32 | -------------------------------------------------------------------------------- /packages/http/test/index.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe after it */ 2 | const net = require('net') 3 | 4 | const should = require('should') 5 | const fetch = require('@plant/test-http/fetch-http') 6 | const Plant = require('@plant/plant') 7 | 8 | const {createServer} = require('..') 9 | 10 | describe('@plant/http', function() { 11 | it('Should server be instance of net.Server', function() { 12 | const plant = new Plant() 13 | const server = createServer(plant) 14 | 15 | should(server).be.instanceOf(net.Server) 16 | }) 17 | 18 | it('Should handle http requests', async function() { 19 | const plant = new Plant() 20 | plant.use(({res}) => { 21 | res.body = 'Hello' 22 | }) 23 | 24 | const server = createServer(plant) 25 | after(function() { 26 | server.close() 27 | }) 28 | 29 | server.listen(0) 30 | 31 | const {text} = await fetch( 32 | new URL(`http://127.0.0.1:${server.address().port}`) 33 | ) 34 | 35 | should(text).be.equal('Hello') 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /packages/http2/.npmignore: -------------------------------------------------------------------------------- 1 | ../-/.npmignore -------------------------------------------------------------------------------- /packages/http2/.testuprc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": "console" 3 | } 4 | -------------------------------------------------------------------------------- /packages/http2/changelog.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.1.0 4 | 5 | - Create HTTP2 transport package 6 | -------------------------------------------------------------------------------- /packages/http2/examples/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 | 9 |

HTTP2

10 |

Do pushes work?

11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/http2/examples/assets/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /packages/http2/examples/push.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const Plant = require('@plant/plant') 5 | const createServer = require('..') 6 | 7 | const plant = new Plant() 8 | 9 | // Logging 10 | plant.use(({req}, next) => { 11 | console.log(req.url + '', 'isPushed:', req.parent !== null) 12 | return next() 13 | }) 14 | 15 | // Server logic 16 | plant.use(async ({req, res, socket, fetch}) => { 17 | let {pathname} = req.url 18 | if (pathname === '/index.html') { 19 | if (socket.canPush) { 20 | await fetch({ 21 | url: new URL('/style.css', req.url), 22 | }) 23 | .then(subRes => socket.push(subRes)) 24 | } 25 | } 26 | 27 | let localPath = path.join(__dirname, 'assets', path.resolve('/', pathname)) 28 | 29 | if (! fs.existsSync(localPath)) { 30 | return 31 | } 32 | 33 | if (fs.statSync(localPath).isDirectory()) { 34 | localPath = path.join(localPath, 'index.html') 35 | } 36 | 37 | if (! fs.existsSync(localPath)) { 38 | return 39 | } 40 | 41 | res.headers.set('content-type', path.basename(pathname) === '.css' ? 'text/css' : 'text/html') 42 | res.body = fs.readFileSync(localPath, 'utf8') 43 | }) 44 | 45 | createServer(plant) 46 | .listen(8080, () => console.log('Listening')) 47 | -------------------------------------------------------------------------------- /packages/http2/license: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Copyright (c) 2017-2018 Rumkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/http2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/http2", 3 | "version": "1.0.1", 4 | "description": "Plant node.js HTTP2 module's server handler", 5 | "main": "src/index.js", 6 | "engines": { 7 | "node": ">=8" 8 | }, 9 | "scripts": { 10 | "lint": "npm run lint:src && npm run lint:test", 11 | "lint:src": "eslint src/**.js", 12 | "lint:test": "eslint test/**.js", 13 | "lint:exampples": "eslint exampples/**.js", 14 | "prepublishOnly": "allow-publish-tag next && npm run lint && npm test", 15 | "test": "testup run test/*.spec.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "github.com/rumkin/plant" 20 | }, 21 | "keywords": [ 22 | "plant", 23 | "http", 24 | "native-http" 25 | ], 26 | "author": "Rumkin (https://rumk.in/)", 27 | "license": "MIT", 28 | "dependencies": { 29 | "@plant/http-adapter": "^1.4.0" 30 | }, 31 | "devDependencies": { 32 | "@plant/plant": "^2.0.0", 33 | "@plant/test-http": "^0.5.3", 34 | "@testup/cli": "^0.4.0", 35 | "@testup/console-reporter": "^0.1.1", 36 | "@testup/core": "^0.1.2", 37 | "allow-publish-tag": "^2.0.0", 38 | "should": "^13.2.2" 39 | }, 40 | "publishConfig": { 41 | "access": "public" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/http2/readme.md: -------------------------------------------------------------------------------- 1 | # Node HTTP2 for Plant 2 | 3 | Plant adapter for node.js http2 module. It creates server listener from plant 4 | instance and `http2.createServer()` options. 5 | 6 | ## Install 7 | 8 | ```bash 9 | npm i @plant/http2 10 | ``` 11 | 12 | ## Usage 13 | 14 | ### Hello World Example 15 | 16 | ```javascript 17 | // Build request handler 18 | const Plant = require('@plant/plant'); 19 | const {createServer} = require('@plant/http2'); 20 | 21 | const plant = new Plant(); 22 | plant.use(({res, socket}) => { 23 | res.body = 'Hello, World!' 24 | }) 25 | 26 | createServer(plant) 27 | .listen(80) 28 | ``` 29 | 30 | ### Enable HTTP1 31 | 32 | HTTP package doesn't support HTTP1 support by default and you should enable it manually: 33 | 34 | ```javascript 35 | createServer(plant, { 36 | allowHTTP1: true, 37 | }) 38 | ``` 39 | 40 | ### HTTP2 Push Example 41 | 42 | ```javascript 43 | plant.use(({res, socket}) => { 44 | // Send HTTP2 push 45 | if (socket.canPush) { 46 | res.push(new Response({ 47 | url: new URL('/style.css', res.url), 48 | headers: { 49 | 'content-type': 'text/css', 50 | }, 51 | body: 'html {color: green}', 52 | })) 53 | } 54 | 55 | res.body = ' { 18 | * res.body = 'Hello, World!' 19 | * }) 20 | * 21 | * createServer(plant).listen(8080) 22 | */ 23 | function createServer(plant, options = {}) { 24 | const server = http2.createServer( 25 | options, createRequestHandler(plant) 26 | ) 27 | 28 | return server 29 | } 30 | 31 | exports.createServer = createServer 32 | -------------------------------------------------------------------------------- /packages/http2/test/index.spec.js: -------------------------------------------------------------------------------- 1 | const net = require('net') 2 | 3 | const should = require('should') 4 | const Plant = require('@plant/plant') 5 | const {Response} = Plant 6 | 7 | const fetch = require('@plant/test-http/fetch-http2') 8 | const {createServer} = require('..') 9 | 10 | module.exports = ({describe, it}) => describe('@plant/http2', function() { 11 | describe('Interface', () => { 12 | it('Should return an instance of net.Server', function() { 13 | const plant = new Plant() 14 | const server = createServer(plant) 15 | 16 | should(server).be.instanceOf(net.Server) 17 | }) 18 | }) 19 | 20 | describe('HTTP2', () => { 21 | it( 22 | 'Should be able to push', 23 | useServer, 24 | async function({runServer}) { 25 | let canPush = null 26 | const plant = new Plant() 27 | plant.use(({res, socket}) => { 28 | canPush = socket.canPush 29 | res.body = 'Hello' 30 | }) 31 | 32 | const server = runServer(plant) 33 | const {text} = await fetch(new URL( 34 | `http://127.0.0.1:${server.address().port}/` 35 | )) 36 | 37 | should(text).be.equal('Hello') 38 | should(canPush).be.equal(true) 39 | } 40 | ) 41 | 42 | it( 43 | 'Should push', 44 | useServer, 45 | async function({runServer}) { 46 | const plant = new Plant() 47 | plant.use(({res, socket}) => { 48 | socket.push( 49 | new Response({ 50 | url: new URL('./push-1.txt', res.url), 51 | body: 'Push 1', 52 | }) 53 | ) 54 | socket.push( 55 | new Response({ 56 | url: new URL('./push-2.txt', res.url), 57 | body: 'Push 2', 58 | }) 59 | ) 60 | res.body = 'Hello' 61 | }) 62 | 63 | const server = runServer(plant) 64 | 65 | const {text, pushed} = await fetch( 66 | new URL(`http://127.0.0.1:${server.address().port}/`), 67 | { 68 | settings: { 69 | enablePush: true, 70 | }, 71 | } 72 | ) 73 | 74 | should(text).be.equal('Hello') 75 | should(pushed[0]).ownProperty('url') 76 | should(pushed[0].url.pathname).which.equal('/push-1.txt') 77 | should(pushed[0].body).be.equal('Push 1') 78 | 79 | should(pushed[1]).ownProperty('url') 80 | should(pushed[1].url.pathname).which.equal('/push-2.txt') 81 | should(pushed[1].body).be.equal('Push 2') 82 | } 83 | ) 84 | }) 85 | }) 86 | 87 | async function useServer(ctx, next) { 88 | let server 89 | 90 | try { 91 | await next({ 92 | ...ctx, 93 | server, 94 | runServer(plant) { 95 | server = createServer(plant) 96 | server.listen(0) 97 | return server 98 | }, 99 | }) 100 | } 101 | finally { 102 | server && server.close() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/https/.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": ["jsdoc"] 5 | }, 6 | "source": { 7 | "include": [".", "package.json", "README.md"], 8 | "includePattern": ".js$", 9 | "excludePattern": "(node_modules/|test/|docs/|.spec.js$)" 10 | }, 11 | "plugins": [ 12 | "plugins/markdown" 13 | ], 14 | "templates": { 15 | "cleverLinks": false, 16 | "monospaceLinks": true, 17 | "useLongnameInNav": false, 18 | "showInheritedInNav": true 19 | }, 20 | "opts": { 21 | "destination": "./tmp/docs/", 22 | "encoding": "utf8", 23 | "private": true, 24 | "recurse": true, 25 | "template": "./node_modules/minami" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/https/.npmignore: -------------------------------------------------------------------------------- 1 | ../-/.npmignore -------------------------------------------------------------------------------- /packages/https/LICENSE: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Copyright (c) 2017-2018 Rumkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/https/changelog.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.2.0 4 | 5 | - Add `ssl` to context. 6 | -------------------------------------------------------------------------------- /packages/https/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/https", 3 | "version": "1.0.1", 4 | "description": "Plant node.js HTTP module's server handler", 5 | "main": "src/index.js", 6 | "engines": { 7 | "node": ">=6" 8 | }, 9 | "scripts": { 10 | "docs": "npm run jsdoc && rm -rf docs && mv tmp/docs/*/*/* docs && rm -rf tmp/docs", 11 | "jsdoc": "jsdoc --configure .jsdoc.json --verbose", 12 | "lint": "npm run lint:src && npm run lint:test", 13 | "lint:src": "eslint src/**.js", 14 | "lint:test": "eslint test/**.js", 15 | "prepublishOnly": "allow-publish-tag next && npm run lint && npm test", 16 | "ssl-cert": "openssl req -newkey rsa:2048 -nodes -keyout var/key.pem -x509 -days 365 -out var/cert.pem", 17 | "test": "mocha test/*.spec.js" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "github.com/rumkin/plant" 22 | }, 23 | "keywords": [ 24 | "plant", 25 | "http", 26 | "native-http" 27 | ], 28 | "author": "Rumkin (https://rumk.in/)", 29 | "license": "MIT", 30 | "dependencies": { 31 | "@plant/http-adapter": "^1.4.0" 32 | }, 33 | "devDependencies": { 34 | "@plant/plant": "^2.0.0-rc12", 35 | "@plant/test-http": "^0.5.3", 36 | "allow-publish-tag": "^2.0.0", 37 | "jsdoc": "^3.6.3", 38 | "minami": "^1.2.3", 39 | "mocha": "^5.2.0", 40 | "should": "^13.2.2" 41 | }, 42 | "publishConfig": { 43 | "access": "public" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/https/readme.md: -------------------------------------------------------------------------------- 1 | # Node HTTPS for Plant 2 | 3 | Plant adapter for node.js https module. It creates server listener from plant instance and https options. 4 | 5 | ## Install 6 | 7 | ```bash 8 | npm i @plant/https 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```javascript 14 | const Plant = require('@plant/plant'); 15 | const {createServer} = require('@plant/https'); 16 | 17 | const plant = new Plant(); 18 | 19 | plant.use(({res}) => { 20 | res.body = 'Ok'; 21 | }); 22 | 23 | createServer(plant, { 24 | // get SSL key and cert somehow 25 | key, 26 | cert, 27 | }) 28 | .listen(443); 29 | ``` 30 | 31 | ## Copyright 32 | 33 | MIT © [Rumkin](https://rumk.in) 34 | -------------------------------------------------------------------------------- /packages/https/src/index.js: -------------------------------------------------------------------------------- 1 | const https = require('https') 2 | const {createRequestHandler} = require('@plant/http-adapter') 3 | 4 | /** 5 | * createServer - creates http server instance with Plant as request handler. 6 | * 7 | * @param {Plant} plant Plant instance. 8 | * @param {Object} options Node.js HTTPS server options. 9 | * @return {net.Server} Http server instance ready to listen port. 10 | * @example 11 | * 12 | * const createServer = require('@plant/https') 13 | * const Plant = require('@plant/plant') 14 | * 15 | * const plant = new Plant() 16 | * 17 | * plant.use(async ({res}) => { 18 | * res.body = 'Hello, World!' 19 | * }) 20 | * 21 | * createServer(plant).listen(8080) 22 | */ 23 | function createServer(plant, options = {}) { 24 | const server = https.createServer( 25 | options, createRequestHandler(plant, { 26 | handlers: [ 27 | (ctx, next) => { 28 | const ssl = new SSL(ctx.httpReq.connection) 29 | return next({...ctx, ssl}) 30 | }, 31 | ], 32 | }) 33 | ) 34 | 35 | return server 36 | } 37 | 38 | class SSL { 39 | constructor(socket) { 40 | // TODO implement other SSL read methods 41 | this.protocol = socket.getProtocol() 42 | this.cipher = socket.getCipher() 43 | this.cert = socket.getCertificate() 44 | this.peerCert = socket.getPeerCertificate(true) 45 | } 46 | } 47 | 48 | exports.createServer = createServer 49 | exports.SSL = SSL 50 | -------------------------------------------------------------------------------- /packages/https/test/fixtures/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC1jCCAb4CCQC55nu5VoBDlzANBgkqhkiG9w0BAQsFADAtMQswCQYDVQQGEwJO 3 | TzEeMBwGCSqGSIb3DQEJARYPYWRtaW5AbG9jYWxob3N0MB4XDTE5MDQwMzA5MjEx 4 | OFoXDTIwMDQwMjA5MjExOFowLTELMAkGA1UEBhMCTk8xHjAcBgkqhkiG9w0BCQEW 5 | D2FkbWluQGxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 6 | APHe9CRFvmOPRameoaiAmL7+AcTjXUM+HtqmT2raK87EWn/l6zREKmx6auJGvNCg 7 | b6f0PHUMjEcbJ4HzigIsn3HEm3oPKRefixyrvNoRU5kQw8gfD9n6A+6QFI8Jzreh 8 | 4+ye7v6X5RFooF6Ain7Md2k/srhoPMmQZDuhm1THtzjiW/41TdteK7oclJrCaJpb 9 | k605EoCaGyfkzy8kuXarUE5vS+fYK1jeQA7dzMjiqA+uz9A8/lw6JEeqnTCRj9cH 10 | Ph0iX2ZFzNyb5GuqXHGzJZNzAha6cd7mwx2WJFyGvO68tORGuMOGntVtEl/wN3u3 11 | WJ4AzNKoGglR7seD8gs/JjcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAsoZse/Om 12 | KtceXo8AIb0m56iXjesQgXGKy4OKxwN+Hb8eu48HNF5iMQ2hRZWRksdFAepuI/hU 13 | J7Qrb6vkmoNoGmgYx6xwyAJ77w7GB3jXy6w90axtGs88YtXcWn46FWfRlmQ54PJC 14 | 64JPxJuVPHmkwAiKKI64KISRrLTTdI3TIcMjycvn1UIjxDzvQUNAbl25C40kAJD/ 15 | oZRh0nqD1fN2hKMI3UwRQb4u817+zrD7m8SUPLzg3ls52kBSzgYNGQpl8qz5cQAf 16 | +/HdBwv1PIAkKQNVbDIEOgjmWBjKmKYGg/kKP0iqlZXkYTZ0ZZRqozX/ZMdvGr5g 17 | mBuoi2OnhtZdRg== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /packages/https/test/fixtures/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDx3vQkRb5jj0Wp 3 | nqGogJi+/gHE411DPh7apk9q2ivOxFp/5es0RCpsemriRrzQoG+n9Dx1DIxHGyeB 4 | 84oCLJ9xxJt6DykXn4scq7zaEVOZEMPIHw/Z+gPukBSPCc63oePsnu7+l+URaKBe 5 | gIp+zHdpP7K4aDzJkGQ7oZtUx7c44lv+NU3bXiu6HJSawmiaW5OtORKAmhsn5M8v 6 | JLl2q1BOb0vn2CtY3kAO3czI4qgPrs/QPP5cOiRHqp0wkY/XBz4dIl9mRczcm+Rr 7 | qlxxsyWTcwIWunHe5sMdliRchrzuvLTkRrjDhp7VbRJf8Dd7t1ieAMzSqBoJUe7H 8 | g/ILPyY3AgMBAAECggEBAIoDgqQ/98evtTx4fB4+Yup43mGOq2T3SXvScnayT1UI 9 | zes1MuT8EIkdwWeknZEeOxhHUUgpBNJ+OCj64sEi3Uh5u44GoJgOPb76cCSuxlkN 10 | K+pBbzYeZ6f1JwYHvqEiC4C1oAI+gkNQxFGoX17DJVA1PLHlKOqLLeao7I2P+IjH 11 | 5UhM9LMjQ27paXua1kbXn5547YwA+lGd5UhOXdmw4Xwye24/P8dIxsj5PxSz7Fj9 12 | gleHdsOwoAMkNwLC+/hEsjnk5aLQDKcn9WW03D/5o4sCr7ttQQF5GOhj74asqChh 13 | ssPafOub7+CW72yMWzXiulWDnrt9p4qfeTKnj8OkGwECgYEA/7zAvWBQVcpfbkLo 14 | DOMaQdoES37GWhBR38pYqT5ZOXEwd9J9Nosfrsd8wbx+rzd1LcUfmAdGM+fKob8o 15 | AXF32KxtCI3WH5xzbqZFKw2jGJPEO9XvJYtUKFGi8taGWO+VuRkXu4OESCO/hplp 16 | E2hWopDoKecqNuJzwsXR5BEd2tcCgYEA8h6N9/V7YDssEvMHK7PfxBQLX+7AZ/+q 17 | 4ARbpJsgNC8CltnVpNRB9mQMZzR1RUpl3Ed0KNYX6/euu8kT6YD3L8eueCwNGmAx 18 | Buo1na7w8orvdfPspa9DO8vVrPJFZxx3Yn7HCiIE8YoCNoJ0y9VlzYQye+DdWYZi 19 | mG1b1mxYA6ECgYBZhByDLV9xaDEXS1wmhqf+PO4b7vhHNkcyaoW2WBirPq+UgZ1K 20 | plAkwbctqWk+s87UgpeYg5NTP+Nx0pOSTcGBmnlf8SXuUrklFhZIB7H7PF4IA859 21 | +dMvecPr9KN6JgLmk533CQYYqkq0NqbNIEyTAt4BLOVd6M5UcyIRDtzAaQKBgBUv 22 | 9U54jeZa5z11RPBfGjrHYkNv2ih/qA1YdEiQRp4qaviWcWquJGOSV7+ESKramtVO 23 | 72xPZ+J/VywZqYNqhLcKq0ra1/6x4jhvsGdvEqi23cOdp9zL3H9UFAvBW69tP0lA 24 | PNy3I2WbpRvuCJh5wSHd6qkKgXQ/HUunbjzyWDVhAoGAdoOD81SFbDv40eMwgbp9 25 | k1dbJ/zKtV11ddswqhUuaHNdrFvuXSz4T/pfedVCPBQVajmRxJMkWmbAUYkv4I+k 26 | LkKthNgqtlDl7CKrukQwnpFtiBo8+G7vMEU9DlfgY+KMnoEo8m7OwlnHFyen9qOT 27 | 4b2/apMn5QZywdLHf5dnpFQ= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /packages/https/test/index.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe it before after */ 2 | 3 | const fs = require('fs') 4 | const net = require('net') 5 | 6 | const should = require('should') 7 | const Plant = require('@plant/plant') 8 | const fetch = require('@plant/test-http/fetch-https') 9 | const {createServer} = require('..') 10 | 11 | const passphrase = '12345678' 12 | let key 13 | let cert 14 | 15 | describe('@plant/https', function() { 16 | before(function() { 17 | key = fs.readFileSync(__dirname + '/fixtures/key.pem') 18 | cert = fs.readFileSync(__dirname + '/fixtures/cert.pem') 19 | }) 20 | 21 | it('Should server be instance of net.Server', function() { 22 | const server = createServer(new Plant(), { 23 | key, 24 | cert, 25 | passphrase, 26 | rejectUnauthorized: false, 27 | }) 28 | 29 | should(server).be.instanceOf(net.Server) 30 | }) 31 | 32 | it('Should handle http requests', async function() { 33 | const plant = new Plant() 34 | plant.use(({res}) => { 35 | res.body = 'Hello' 36 | }) 37 | 38 | const server = createServer(plant, { 39 | key, 40 | cert, 41 | passphrase, 42 | rejectUnauthorized: false, 43 | }) 44 | 45 | after(function() { 46 | server.close() 47 | }) 48 | 49 | server.listen(0) 50 | 51 | const {status, text} = await fetch(`https://127.0.0.1:${server.address().port}`, { 52 | rejectUnauthorized: false, 53 | }) 54 | 55 | should(status).be.equal(200) 56 | should(text).be.equal('Hello') 57 | }) 58 | 59 | it('Should add ssl context variable', async function() { 60 | const plant = new Plant() 61 | let ssl = null 62 | 63 | plant.use(({res, ssl: _ssl}) => { 64 | ssl = _ssl 65 | res.body = 'Hello' 66 | }) 67 | 68 | const server = createServer(plant, { 69 | key, 70 | cert, 71 | passphrase, 72 | rejectUnauthorized: false, 73 | }) 74 | 75 | after(function() { 76 | server.close() 77 | }) 78 | 79 | server.listen(0) 80 | 81 | const {status, text} = await fetch(`https://127.0.0.1:${server.address().port}`, { 82 | rejectUnauthorized: false, 83 | }) 84 | 85 | should(status).be.equal(200) 86 | should(text).be.equal('Hello') 87 | 88 | should(ssl).be.an.Object() 89 | .and.ownProperty('peerCert') 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /packages/https2/.npmignore: -------------------------------------------------------------------------------- 1 | ../-/.npmignore -------------------------------------------------------------------------------- /packages/https2/.testuprc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": "console" 3 | } 4 | -------------------------------------------------------------------------------- /packages/https2/changelog.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.1.0 4 | 5 | - Create HTTPS2 transport package 6 | -------------------------------------------------------------------------------- /packages/https2/examples/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 | 9 |

HTTP2

10 |

Do pushes work?

11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/https2/examples/assets/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /packages/https2/examples/push.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const Plant = require('@plant/plant') 5 | const createServer = require('..') 6 | 7 | const plant = new Plant() 8 | 9 | // Logging 10 | plant.use(({req}, next) => { 11 | console.log(req.url + '', 'isPushed:', req.parent !== null) 12 | return next() 13 | }) 14 | 15 | // Server logic 16 | plant.use(async ({req, res, socket, fetch}) => { 17 | let {pathname} = req.url 18 | if (pathname === '/index.html') { 19 | if (socket.canPush) { 20 | await fetch({ 21 | url: new URL('/style.css', req.url), 22 | }) 23 | .then(subRes => socket.push(subRes)) 24 | } 25 | } 26 | 27 | let localPath = path.join(__dirname, 'assets', path.resolve('/', pathname)) 28 | 29 | if (! fs.existsSync(localPath)) { 30 | return 31 | } 32 | 33 | if (fs.statSync(localPath).isDirectory()) { 34 | localPath = path.join(localPath, 'index.html') 35 | } 36 | 37 | if (! fs.existsSync(localPath)) { 38 | return 39 | } 40 | 41 | res.headers.set('content-type', path.basename(pathname) === '.css' ? 'text/css' : 'text/html') 42 | res.body = fs.readFileSync(localPath, 'utf8') 43 | }) 44 | 45 | createServer(plant, { 46 | key: fs.readFileSync('var/ssl/key.pem'), 47 | cert: fs.readFileSync('var/ssl/cert.pem'), 48 | }) 49 | .listen(8080, () => console.log('Listening')) 50 | -------------------------------------------------------------------------------- /packages/https2/license: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Copyright (c) 2017-2018 Rumkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/https2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/https2", 3 | "version": "1.0.2", 4 | "description": "Plant node.js HTTP2 module's SSL server handler", 5 | "main": "src/index.js", 6 | "engines": { 7 | "node": ">=8" 8 | }, 9 | "scripts": { 10 | "lint": "npm run lint:src && npm run lint:test", 11 | "lint:src": "eslint src/**.js", 12 | "lint:test": "eslint test/**.js", 13 | "prepublishOnly": "allow-publish-tag next && npm test", 14 | "ssl": "utils/ssl.sh var/ssl", 15 | "test": "testup run test/*.spec.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "github.com/rumkin/plant" 20 | }, 21 | "keywords": [ 22 | "plant", 23 | "http", 24 | "native-http" 25 | ], 26 | "author": "Rumkin (https://rumk.in/)", 27 | "license": "MIT", 28 | "dependencies": { 29 | "@plant/http-adapter": "^1.4.0" 30 | }, 31 | "devDependencies": { 32 | "@plant/plant": "^2.0.0", 33 | "@plant/test-http": "^0.5.3", 34 | "@testup/cli": "^0.4.0", 35 | "@testup/console-reporter": "^0.1.1", 36 | "@testup/core": "^0.1.2", 37 | "allow-publish-tag": "^2.0.0", 38 | "should": "^13.2.2" 39 | }, 40 | "publishConfig": { 41 | "access": "public" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/https2/readme.md: -------------------------------------------------------------------------------- 1 | # Node HTTP2 TLS/SSL server for Plant 2 | 3 | Plant adapter for node.js http2 module. It creates server listener from plant 4 | instance and `http2.createSecureServer()` options. 5 | 6 | ## Install 7 | 8 | ```bash 9 | npm i @plant/https2 10 | ``` 11 | 12 | ## Usage 13 | 14 | ### Hello World Example 15 | 16 | ```javascript 17 | // Build request handler 18 | const Plant = require('@plant/plant'); 19 | const {createServer} = require('@plant/https2'); 20 | 21 | const plant = new Plant(); 22 | plant.use(({res, socket}) => { 23 | res.body = 'Hello, World!' 24 | }) 25 | 26 | createServer(plant, { 27 | // get SSL key and cert somehow 28 | key, 29 | cert, 30 | }).listen(443) 31 | ``` 32 | 33 | ### Enable HTTP1 34 | 35 | HTTP package doesn't support HTTP1 support by default and you should enable it manually: 36 | 37 | ```javascript 38 | createServer(plant, { 39 | allowHTTP1: true, 40 | }) 41 | ``` 42 | 43 | ### HTTP2 Push Example 44 | 45 | ```javascript 46 | plant.use(({res, socket}) => { 47 | // Send HTTP2 push 48 | if (socket.canPush) { 49 | res.push(new Response({ 50 | url: new URL('/style.css', res.url), 51 | headers: { 52 | 'content-type': 'text/css', 53 | }, 54 | body: 'html {color: green}', 55 | })) 56 | } 57 | 58 | res.body = ' { 18 | * res.body = 'Hello, World!' 19 | * }) 20 | * 21 | * createServer(plant).listen(8080) 22 | */ 23 | function createServer(plant, options = {}) { 24 | const server = http2.createSecureServer( 25 | options, createRequestHandler(plant, { 26 | handlers: [ 27 | (ctx, next) => { 28 | const ssl = new SSL(ctx.httpReq.connection) 29 | return next({...ctx, ssl}) 30 | }, 31 | ], 32 | }) 33 | ) 34 | 35 | return server 36 | } 37 | 38 | class SSL { 39 | constructor(socket) { 40 | // TODO implement other SSL read methods 41 | this.protocol = socket.getProtocol() 42 | this.cipher = socket.getCipher() 43 | this.cert = socket.getCertificate() 44 | this.peerCert = socket.getPeerCertificate(true) 45 | } 46 | } 47 | 48 | exports.createServer = createServer 49 | exports.SSL = SSL 50 | -------------------------------------------------------------------------------- /packages/https2/test/fixtures/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICvjCCAaYCCQDQB+GpXnGY5DANBgkqhkiG9w0BAQsFADAhMQ4wDAYDVQQKDAVQ 3 | bGFudDEPMA0GA1UECwwGU2VydmVyMB4XDTE5MDQxOTE2MTYxMVoXDTIwMDQxODE2 4 | MTYxMVowITEOMAwGA1UECgwFUGxhbnQxDzANBgNVBAsMBlNlcnZlcjCCASIwDQYJ 5 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKhwRr4P+7N6yR/Ff/e/wy8bNW04B945 6 | Sh/WO1+m6e42BD/KlqxsG8xybWxyv/ntYwJ06GSEMePVVb09hWZLTstRv96BUjFv 7 | 8Ul5PmypRgw9IURLzIRIcpNa68FGrt+uhrPS9baj3Jzh7iNbfcA0Ba/hy0MDbLph 8 | by5tSShyQA/VYWaX/dv2NBdjVfES2nBIqT3k0D7md2mD/qvuSrRi6h9t4sId73SU 9 | E0k9OXi4Pq1DcDvXBqFAi/FG7Tp7KkPnav9CusSbQsJA5W1jWgNcXTA5vSrKStNb 10 | /jPoWwRBfmz9TVbpECdaxygDs0SCQTyN3sxKI6PvT6uGkSB3dVoBWqMCAwEAATAN 11 | BgkqhkiG9w0BAQsFAAOCAQEADiBtZgjbob4ztyFymSN+JMsvRgJsQPvHeNKwf026 12 | 0mSNLwGW4CuB54M8MW8y42+Qr233CTPGUixvWoMPTgdMPrbwK5JWt9wWw1Pw/Hni 13 | klwV85CSq/r6FUrjDkjXtb+fitQxVcUUMSb/h3u3sFQIlBBNUivR+7CHKmc4pJIC 14 | VKpV1LBlP29Q9hDmFEyKKpkPwHgoaQfqSnaDDGES3CjQUn+DEgJQl6nLldJWRiRU 15 | rKk1nkODiaXBewIpXhv1sQUK3oVoZs/aUv//BypvQ7etvYHuvCdr2lgRD8CSwtk2 16 | 1CxShOOZbA11mgNupixT21OwtbAvo4m+VyGR9DPFpaA/RA== 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /packages/https2/test/fixtures/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCocEa+D/uzeskf 3 | xX/3v8MvGzVtOAfeOUof1jtfpunuNgQ/ypasbBvMcm1scr/57WMCdOhkhDHj1VW9 4 | PYVmS07LUb/egVIxb/FJeT5sqUYMPSFES8yESHKTWuvBRq7froaz0vW2o9yc4e4j 5 | W33ANAWv4ctDA2y6YW8ubUkockAP1WFml/3b9jQXY1XxEtpwSKk95NA+5ndpg/6r 6 | 7kq0YuofbeLCHe90lBNJPTl4uD6tQ3A71wahQIvxRu06eypD52r/QrrEm0LCQOVt 7 | Y1oDXF0wOb0qykrTW/4z6FsEQX5s/U1W6RAnWscoA7NEgkE8jd7MSiOj70+rhpEg 8 | d3VaAVqjAgMBAAECggEAXhj7LEq5jnbVzQ4Eg195puNIYY+ftaHDqy1/Vdxla1J5 9 | 5TlEG2b50KlMP/2LChB383NkMGM5i9IuZ93qnE8N4b/1tFQCmuOypB07pnCaVVQB 10 | NaoywuPGPlPYyMy3/PX/Ao6j/jhkkrAU3WPLSIjHdI5rgzBymVy9Q+6BpDrPVwgx 11 | jiy2A0a1jXGz068dgwBoV0qp0TELrdHIk/RLDbEtzWUD8bhHwZjkdh8SAYnVrUMw 12 | vg/82ANCD3o7b4Ok/5L4t5m7F0R/ADg9XNABaiCfMXz2UvtbAg4UxffaTDYuN92g 13 | W2Rz19lWokk1nX0bVE3A/tGPiKoYZB4BelivMVnywQKBgQDW/zoelLOsIc6KflhU 14 | nhlTXg+PHtM2A40OrsvHI/yBIklJDB2F0mVH02M7dMeRgP1mNJfIK57y06HHXtdl 15 | 17xCuxgDdnUH/HJIMDcIUac2NmjMtNrRrsAHF6IRj4pbT3YVFj0L1qMvlo41xsfn 16 | pNc81k5GZO50JhmE7wxeb5ekOQKBgQDIj+8OzDgmGESZSkPQZkRmkuzhY3XeM0ZL 17 | 8Ep9D7aSndxnNTPy+dxpaHUpyFbhbOA31S1vdgzREAWtKN5UqfrvzpF0iU59Makn 18 | FzWsU6I8ac2HRQ/Y7UdsWyT5WjKMGLf31TzKI0JifFftfOz+/cO9s7xGOd+mpPVN 19 | InCWsRGNuwKBgDdG89h8/x0YrBPrnCZVZ8mJe5KeqEtQ6mmGA5q14+wHtrPzS3vm 20 | tmebL/5Pbig48+3dQ9ERdhKU2xl5hwQGTb8Sf4AUas6c1307+EpJRCaqIpPPRBt5 21 | RKIOL3s4XqhPa9rMFvH+Q4KuwO2OqEMknLpll0Z+GNkAGruVAqcdJe3xAoGANBR8 22 | JUGOiwXeOlf4iBMmS+R3MofbQZna9Tkufo8n/6aSZxJ/rOaI/64qTnFBbkQRbS4k 23 | ID9tUJRyhOaJ5T5GdSMUzkghY40TuZzjSR5mkH2A61FZriDfXRnF3iI34f1BOE/c 24 | +zhwspZLVtYLzKMkwwv7Jdk9ZE6NjDwXNGpCfqUCgYBWztPYaqomkt7eIsU1FtUQ 25 | ztGaqLJWxtaZ0kT97B0rD4sNxEZVEIdC8PQi1LnoRH0mUmqENK4rqzAvN4nM6LcR 26 | MycsFJ/hs+NutZOrKjmKmN8jM9PSj/RvT/UzGoyghZFwGSlF/so+gyDlHD2m/Uln 27 | mFVipKTjLWSBU5+kXJ72PQ== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /packages/https2/test/index.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const net = require('net') 3 | 4 | const should = require('should') 5 | const Plant = require('@plant/plant') 6 | const {Response} = Plant 7 | 8 | const fetch = require('@plant/test-http/fetch-http2') 9 | const {createServer} = require('..') 10 | 11 | module.exports = ({describe, use, it}) => describe('@plant/https2', function() { 12 | use(function(ctx, next) { 13 | return next({ 14 | ...ctx, 15 | ssl: { 16 | key: fs.readFileSync(__dirname + '/fixtures/key.pem'), 17 | cert: fs.readFileSync(__dirname + '/fixtures/cert.pem'), 18 | }, 19 | }) 20 | }) 21 | 22 | describe('Interface', () => { 23 | it('Should return an instance of net.Server', function({ssl}) { 24 | const plant = new Plant() 25 | const server = createServer(plant, ssl) 26 | 27 | should(server).be.instanceOf(net.Server) 28 | }) 29 | }) 30 | 31 | describe('SSL', () => { 32 | it( 33 | 'Should handle request', 34 | useServer, 35 | async function({runServer}) { 36 | const plant = new Plant() 37 | plant.use(({res}) => { 38 | res.body = 'Hello' 39 | }) 40 | 41 | const server = runServer(plant) 42 | 43 | const {text} = await fetch( 44 | new URL(`https://localhost:${server.address().port}/`), 45 | { 46 | rejectUnauthorized: false, 47 | } 48 | ) 49 | 50 | should(text).be.equal('Hello') 51 | } 52 | ) 53 | 54 | it( 55 | 'Should provide ssl certificate', 56 | useServer, 57 | async function({runServer}) { 58 | let cert 59 | const plant = new Plant() 60 | plant.use(({res, ssl}) => { 61 | cert = ssl.cert 62 | res.body = '' 63 | }) 64 | 65 | const server = runServer(plant) 66 | server.on('error', () => {}) 67 | 68 | await fetch( 69 | new URL(`https://localhost:${server.address().port}/`), 70 | { 71 | rejectUnauthorized: false, 72 | }, 73 | ) 74 | 75 | should(cert).be.an.Object() 76 | .and.ownProperty('pubkey') 77 | 78 | should(cert.pubkey.toString('base64')).be.equal(` 79 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqHBGvg/7s3rJH8V/97/D 80 | Lxs1bTgH3jlKH9Y7X6bp7jYEP8qWrGwbzHJtbHK/+e1jAnToZIQx49VVvT2FZktO 81 | y1G/3oFSMW/xSXk+bKlGDD0hREvMhEhyk1rrwUau366Gs9L1tqPcnOHuI1t9wDQF 82 | r+HLQwNsumFvLm1JKHJAD9VhZpf92/Y0F2NV8RLacEipPeTQPuZ3aYP+q+5KtGLq 83 | H23iwh3vdJQTST05eLg+rUNwO9cGoUCL8UbtOnsqQ+dq/0K6xJtCwkDlbWNaA1xd 84 | MDm9KspK01v+M+hbBEF+bP1NVukQJ1rHKAOzRIJBPI3ezEojo+9Pq4aRIHd1WgFa 85 | owIDAQAB 86 | `.replace(/\s/g, '') 87 | ) 88 | } 89 | ) 90 | }) 91 | 92 | describe('HTTP2', () => { 93 | it( 94 | 'Should be able to push', 95 | useServer, 96 | async function({runServer}) { 97 | let canPush = null 98 | const plant = new Plant() 99 | plant.use(({res, socket}) => { 100 | canPush = socket.canPush 101 | res.body = 'Hello' 102 | }) 103 | 104 | const server = runServer(plant) 105 | 106 | const {text} = await fetch( 107 | new URL(`https://localhost:${server.address().port}/`), 108 | { 109 | rejectUnauthorized: false, 110 | }, 111 | ) 112 | 113 | should(text).be.equal('Hello') 114 | should(canPush).be.equal(true) 115 | } 116 | ) 117 | 118 | it( 119 | 'Should push', 120 | useServer, 121 | async function({runServer}) { 122 | const plant = new Plant() 123 | plant.use(({res, socket}) => { 124 | socket.push( 125 | new Response({ 126 | url: new URL('./push-1.txt', res.url), 127 | body: 'Push 1', 128 | }) 129 | ) 130 | socket.push( 131 | new Response({ 132 | url: new URL('./push-2.txt', res.url), 133 | body: 'Push 2', 134 | }) 135 | ) 136 | res.body = 'Hello' 137 | }) 138 | 139 | const server = runServer(plant) 140 | 141 | const {text, pushed} = await fetch( 142 | new URL(`https://localhost:${server.address().port}/`), 143 | { 144 | rejectUnauthorized: false, 145 | settings: { 146 | enablePush: true, 147 | }, 148 | } 149 | ) 150 | 151 | should(text).be.equal('Hello') 152 | should(pushed[0]).ownProperty('url') 153 | should(pushed[0].url.pathname).which.equal('/push-1.txt') 154 | should(pushed[0].text).be.equal('Push 1') 155 | 156 | should(pushed[1]).ownProperty('url') 157 | should(pushed[1].url.pathname).which.equal('/push-2.txt') 158 | should(pushed[1].text).be.equal('Push 2') 159 | } 160 | ) 161 | }) 162 | }) 163 | 164 | async function useServer(ctx, next) { 165 | let server 166 | const {ssl} = ctx 167 | 168 | try { 169 | await next({ 170 | ...ctx, 171 | runServer(plant) { 172 | server = createServer(plant, { 173 | rejectUnauthorized: false, 174 | ...ssl, 175 | }) 176 | server.listen(0) 177 | return server 178 | }, 179 | }) 180 | } 181 | finally { 182 | server && server.close() 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /packages/https2/utils/ssl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR=$1 4 | 5 | if [ ! -e "$DIR" ] 6 | then 7 | mkdir -p $DIR 8 | fi 9 | 10 | openssl req -newkey rsa:2048 -nodes -keyout $DIR/key.pem -x509 -days 365 -out $DIR/cert.pem 11 | -------------------------------------------------------------------------------- /packages/node-stream-utils/.npmignore: -------------------------------------------------------------------------------- 1 | ../-/.npmignore -------------------------------------------------------------------------------- /packages/node-stream-utils/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/node-stream-utils", 3 | "version": "0.1.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "allow-publish-tag": { 8 | "version": "2.1.1", 9 | "resolved": "https://registry.npmjs.org/allow-publish-tag/-/allow-publish-tag-2.1.1.tgz", 10 | "integrity": "sha512-w26dHOZT3Hd70UK/Vukci1wMeces4RzLrmnE+qYinb3cisQU948s/3QoqG38WAs8lW2NwT4MyHeTG4RkZGzwFw==", 11 | "dev": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/node-stream-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/node-stream-utils", 3 | "version": "0.1.2", 4 | "description": "Node stream utils required to wrap native streams with simple wrappers", 5 | "main": "src/index.js", 6 | "engines": { 7 | "node": ">=8.0" 8 | }, 9 | "scripts": { 10 | "lint": "npm run lint:src && npm run lint:test", 11 | "lint:src": "eslint src/**.js", 12 | "lint:test": "eslint test/**.js", 13 | "prepublishOnly": "allow-publish-tag next && npm run lint && npm test", 14 | "test": "echo 'No tests yet'" 15 | }, 16 | "license": "MIT", 17 | "dependencies": {}, 18 | "devDependencies": { 19 | "allow-publish-tag": "^2.1.1" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "directories": { 25 | "test": "test" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/rumkin/plant.git" 30 | }, 31 | "keywords": [ 32 | "@plant/plant", 33 | "web", 34 | "http", 35 | "handler" 36 | ], 37 | "author": "rumkin", 38 | "homepage": "https://github.com/rumkin/plant/tree/master/packages/node-stream-utils", 39 | "bugs": "https://github.com/rumkin/plant/issues" 40 | } 41 | -------------------------------------------------------------------------------- /packages/node-stream-utils/readme.md: -------------------------------------------------------------------------------- 1 | # Node Stream Utils 2 | 3 | Utils for wrapping Node.js' stream into WebStream API and back. 4 | 5 | ## Install 6 | 7 | ``` 8 | npm i @plant/node-stream-util 9 | ``` 10 | 11 | ## Usage 12 | 13 | Make WebAPI Stream from Node.js': 14 | 15 | ```js 16 | const fs = require('fs') 17 | const {NodeToWebStream} = require('@plant/node-stream-utils') 18 | 19 | const webStream = new NodeToWebStream( 20 | fs.createReadStream(__filename) 21 | ) 22 | ``` 23 | 24 | Wrap WebAPI stream into Node.js': 25 | ```js 26 | const fs = require('fs') 27 | const {WebToNodeStream} = require('@plant/node-stream-utils') 28 | 29 | async function plantHandler({req}) { 30 | new WebToNodeStream(req.body) 31 | .pipe(fs.createWriteStream('index.html')) 32 | } 33 | ``` 34 | 35 | ## License 36 | 37 | MIT © [Rumkin](https://rumk.in) 38 | -------------------------------------------------------------------------------- /packages/node-stream-utils/src/index.js: -------------------------------------------------------------------------------- 1 | const {Readable} = require('stream') 2 | 3 | class NodeToWebStream { 4 | constructor(stream) { 5 | this.stream = stream 6 | this._disturbed = false 7 | 8 | stream.pause() 9 | } 10 | 11 | getReader() { 12 | return new StreamReader(this.stream) 13 | } 14 | } 15 | 16 | class StreamReader { 17 | constructor(stream) { 18 | this.stream = stream 19 | } 20 | 21 | [Symbol.asyncIterator]() { 22 | return { 23 | next: this.read.bind(this), 24 | } 25 | } 26 | 27 | read() { 28 | const {stream} = this 29 | stream._disturbed = true 30 | 31 | return new Promise((resolve, reject) => { 32 | stream.resume() 33 | stream[Symbol.asyncIterator]() 34 | .next() 35 | .then(resolve, reject) 36 | }) 37 | } 38 | 39 | releaseLock() { 40 | this.stream = null 41 | } 42 | } 43 | 44 | async function * iterateReader(reader) { 45 | for (const value of reader) { 46 | yield value 47 | } 48 | } 49 | 50 | class WebToNodeStream extends Readable { 51 | constructor(stream, options) { 52 | super(options) 53 | 54 | this._iterator = iterateReader(stream.getReader()) 55 | this._disturbed = false 56 | } 57 | 58 | _read() { 59 | if (! this._reading) { 60 | this._next() 61 | } 62 | } 63 | 64 | async _next() { 65 | this._reading = true 66 | try { 67 | const {value, done} = await this._iterator.next() 68 | if (done) { 69 | this.push(null) 70 | } 71 | else if (this.push(await value)) { 72 | this._next() 73 | } 74 | else { 75 | this._reading = false 76 | } 77 | } 78 | catch (err) { 79 | this.destroy(err) 80 | } 81 | } 82 | } 83 | 84 | exports.NodeToWebStream = NodeToWebStream 85 | exports.WebToNodeStream = WebToNodeStream 86 | -------------------------------------------------------------------------------- /packages/node-stream-utils/test/index.spec.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumkin/plant/061f376dbdee578ce141abec39a5bc06c6a86f97/packages/node-stream-utils/test/index.spec.js -------------------------------------------------------------------------------- /packages/plant/.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": ["jsdoc"] 5 | }, 6 | "source": { 7 | "include": ["src", "package.json", "README.md"], 8 | "includePattern": ".js$", 9 | "excludePattern": "(node_modules/|test/|docs)" 10 | }, 11 | "plugins": [ 12 | "plugins/markdown" 13 | ], 14 | "templates": { 15 | "cleverLinks": false, 16 | "monospaceLinks": true, 17 | "useLongnameInNav": false, 18 | "showInheritedInNav": true 19 | }, 20 | "opts": { 21 | "destination": "./tmp/docs/", 22 | "encoding": "utf8", 23 | "private": true, 24 | "recurse": true, 25 | "template": "./node_modules/minami" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/plant/.npmignore: -------------------------------------------------------------------------------- 1 | ../-/.npmignore -------------------------------------------------------------------------------- /packages/plant/LICENSE: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Copyright (c) 2017-2019 Rumkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/plant/changelog.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ### v2.0 4 | 5 | - `Request.body` is a Web API ReadableStream. 6 | - `Request.sender` renamed to `Request.peer`. 7 | - `Response.stream()` accepts ReadableStream only. 8 | 9 | ### v1.0.0 10 | 11 | - Updated: 12 | - `Response.headers` and `Request.headers` now are WebAPI Headers objects. 13 | - Request body param renamed and separated by destination: 14 | - `body` - Buffer or null, 15 | - `data` - Object of values from `Request.body`, 16 | - `stream` - Readable stream. 17 | - Request's `ip` property renamed to peer. Now it's a URI which identify request sender. 18 | - Request's `url` property now is an object returned by `url.parse()`. 19 | - Removed: 20 | - `Request.query` property. Use `Request.url.query` instead. 21 | - Added: 22 | - `Response.redirect()` method. 23 | - `Request.pathname` property. It specifies current processing url (without `.baseUrl`). 24 | - `Request.baseUrl` property. It specifies already processed url (without `.pathname`). It's using for nested handlers resolution. 25 | - Documentation: 26 | - Rewrite readme. 27 | - Add JSDoc comments to ~90% of code. 28 | - Utils: 29 | - Remove redundant .dockerignore and .npmrc. 30 | -------------------------------------------------------------------------------- /packages/plant/dev/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rumkin/plant/061f376dbdee578ce141abec39a5bc06c6a86f97/packages/plant/dev/cover.png -------------------------------------------------------------------------------- /packages/plant/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/plant", 3 | "version": "2.5.0", 4 | "description": "WebAPI charged HTTP2-ready web server for node.js and browser", 5 | "main": "src/index.js", 6 | "engines": { 7 | "node": ">= 11" 8 | }, 9 | "scripts": { 10 | "build": "npm run clean && npm run dist && npm run compile && npm run minify", 11 | "clean": "rm -rf dist", 12 | "compile": "browserify -s Plant src/server.js > dist/plant.js", 13 | "dist": "mkdir dist", 14 | "docs": "npm run jsdoc && rm -rf docs && mv ./tmp/docs/@plant/plant/* docs && rm -rf tmp/docs", 15 | "jsdoc": "jsdoc --configure .jsdoc.json --verbose", 16 | "lint": "npm run lint:src && npm run lint:test", 17 | "lint:src": "eslint src/**.js", 18 | "lint:test": "eslint test/**.js", 19 | "minify": "babel-minify dist/plant.js -o dist/plant.min.js", 20 | "next": "npm publish --tag next", 21 | "prepublishOnly": "npm run lint && npm run build && npm run docs", 22 | "start": "node .", 23 | "test": "mocha test/index.js" 24 | }, 25 | "license": "MIT", 26 | "devDependencies": { 27 | "babel-minify": "^0.5.0", 28 | "browserify": "^16.2.3", 29 | "jsdoc": "^3.6.3", 30 | "minami": "^1.2.3", 31 | "mocha": "^6.2.0", 32 | "should": "^11.2.1" 33 | }, 34 | "dependencies": { 35 | "@plant/flow": "^1.0.0", 36 | "cookie": "^0.3.1", 37 | "escape-string-regexp": "^2.0.0", 38 | "eventemitter3": "^3.1.0", 39 | "lodash.escaperegexp": "^4.1.2", 40 | "lodash.isobject": "^3.0.2", 41 | "lodash.isplainobject": "^4.0.6", 42 | "lodash.isstring": "^4.0.1", 43 | "statuses": "^1.5.0" 44 | }, 45 | "directories": { 46 | "test": "test", 47 | "docs": "docs" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "https://github.com/rumkin/plant.git" 52 | }, 53 | "keywords": [ 54 | "server", 55 | "web", 56 | "http", 57 | "https", 58 | "api" 59 | ], 60 | "author": "rumkin", 61 | "homepage": "https://github.com/rumkin/plant", 62 | "bugs": "https://github.com/rumkin/plant/issues", 63 | "publishConfig": { 64 | "access": "public" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/plant/src/handlers/cookie-handler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Plant.Handlers.Cookie 3 | * @description Common Http Request and Response handlers. 4 | */ 5 | 6 | const cookie = require('cookie') 7 | 8 | /** 9 | * Adds methods to set cookies and current cookie data object. 10 | * 11 | * @param {Request} req - Plant.Request instance 12 | * @param {Response} res - Plant.Response instance 13 | * @return {void} 14 | */ 15 | function addCookieSupport(req, res) { 16 | if (req.headers.has('cookie')) { 17 | req.cookies = req.headers.raw('cookie') 18 | .reduce(function (all, header) { 19 | return { 20 | ...all, 21 | ...cookie.parse(header), 22 | } 23 | }, {}) 24 | req.registeredCookies = Object.getOwnPropertyNames(req.cookies) 25 | } 26 | else { 27 | req.cookies = {} 28 | req.registeredCookies = [] 29 | } 30 | 31 | // Set new cookie value 32 | res.setCookie = responseSetCookie 33 | 34 | // Remove cookie by name 35 | res.clearCookie = responseClearCookie 36 | 37 | // Remove all cookies 38 | res.clearCookies = responseClearCookies 39 | } 40 | 41 | /** 42 | * Response extension. Add set-cookie header to response headers. 43 | * 44 | * @param {String} name Cookie name. 45 | * @param {String} value Cookie value. 46 | * @param {Object} options Cookie options like expiration, domain, etc. 47 | * @return {Response} Returns `this`. 48 | */ 49 | function responseSetCookie(name, value, options) { 50 | const opts = Object.assign({path: '/'}, options) 51 | const header = cookie.serialize(name, String(value), opts) 52 | 53 | this.headers.append('set-cookie', header) 54 | 55 | return this 56 | } 57 | 58 | /** 59 | * Response extension. Add set-cookie header which erases cookie. 60 | * 61 | * @param {String} name Cookie name. 62 | * @param {Object} options Header options like expiration, domain, etc. 63 | * @return {Response} Returns `this`. 64 | */ 65 | function responseClearCookie(name, options) { 66 | const opts = Object.assign({expires: new Date(0), path: '/'}, options) 67 | const header = cookie.serialize(name, '', opts) 68 | 69 | this.headers.append('set-cookie', header) 70 | 71 | return this 72 | } 73 | 74 | /** 75 | * Response extension. Remove all cookies passed in request. 76 | * 77 | * @param {Object} options Header options. 78 | * @return {Response} Returns `this`. 79 | */ 80 | function responseClearCookies(options) { 81 | this.registeredCookies.forEach(function (cookieName) { 82 | this.clearCookie(cookieName, options) 83 | }, this) 84 | 85 | return this 86 | } 87 | 88 | /** 89 | * Add cookie controlls to request and response objects. 90 | * 91 | * @param {Plant.Context} context Plant Context. 92 | * @param {function(Object?)} next Next cascade handler emitter. 93 | * @returns {void} Returns nothing. 94 | */ 95 | function cookieHandler({req, res}, next) { 96 | addCookieSupport(req, res) 97 | return next() 98 | } 99 | 100 | module.exports = cookieHandler 101 | -------------------------------------------------------------------------------- /packages/plant/src/headers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Http.Headers 3 | * @description WebAPI Headers implementation. 4 | */ 5 | 6 | /** 7 | * @const {String} MODE_NONE - None mode flag. This mode allow Headers modification. 8 | */ 9 | const MODE_NONE = 'none' 10 | /** 11 | * @const {String} MODE_IMMUTABLE - Immutable mode falg. This mode deny Headers modification. 12 | */ 13 | const MODE_IMMUTABLE = 'immutable' 14 | 15 | /** 16 | * @typedef {ObjectInitials|ArrayInitials} HeadersInitials - Initial values for Headers. Could be object of strings or entires list. 17 | */ 18 | /** 19 | * @typedef {Object.} ObjectInitials Header initials as Object 20 | */ 21 | /** 22 | * @typedef {Array.>} ArrayInitials Header initials as array of entries. 23 | */ 24 | 25 | /** 26 | * @class 27 | * @classdesc WebAPI Headers implementation. 28 | */ 29 | class Headers { 30 | /** 31 | * @param {HeadersInitials} initials = [] Header initial value. 32 | * @param {String} mode = MODE_NONE Headers object mode. 33 | * @return {Headers} Headers instance 34 | * @constructor 35 | */ 36 | constructor(initials = [], mode = MODE_NONE) { 37 | if (! Array.isArray(initials)) { 38 | initials = Object.entries(initials) 39 | } 40 | 41 | this._headers = new Map(initials.map(function([key, value]) { 42 | return ([key, [value]]) 43 | })) 44 | this._mode = mode 45 | 46 | if (mode !== MODE_NONE) { 47 | this.set = 48 | this.append = 49 | this.delete = 50 | this.wrongMode 51 | } 52 | } 53 | 54 | /** 55 | * Headers mode getter. 56 | * 57 | * @return {String} Should returns MODE_NONE or MODE_IMMUTABLE value. 58 | */ 59 | get mode() { 60 | return this._mode 61 | } 62 | 63 | /** 64 | * Set header value. Overwrite previous values. 65 | * 66 | * @param {String} _name Header name. 67 | * @param {String} _value Header value. 68 | * @returns {void} 69 | * @throws {Error} Throws if current mode is immutable. 70 | */ 71 | set(_name, _value) { 72 | const name = normalizedName(_name) 73 | const value = normalizedValue(_value) 74 | 75 | this._headers.set(name, [value]) 76 | } 77 | 78 | /** 79 | * Append header. Preserve previous values. 80 | * 81 | * @param {String} _name Header name. 82 | * @param {String} _value Header value. 83 | * @returns {void} 84 | * @throws {Error} Throws if current mode is immutable. 85 | */ 86 | append(_name, _value) { 87 | const name = normalizedName(_name) 88 | const value = normalizedValue(_value) 89 | 90 | if (this._headers.has(name)) { 91 | this._headers.get(name).push(value) 92 | } 93 | else { 94 | this._headers.set(name, [value]) 95 | } 96 | } 97 | 98 | /** 99 | * Remove header from headers list 100 | * 101 | * @param {String} _name Header name. 102 | * @return {void} 103 | * @throws {Error} Throws if current mode is immutable. 104 | */ 105 | delete(_name) { 106 | this._headers.delete( 107 | normalizedName(_name) 108 | ) 109 | } 110 | 111 | /** 112 | * Specify whether header with name is contained in Headers list. 113 | * 114 | * @example 115 | * 116 | * headers.set('accept', 'text/plain') 117 | * headers.has('accept') 118 | * // > true 119 | * headers.delete('accept') 120 | * headers.has('accept') 121 | * // > false 122 | * @param {String} _name Header name 123 | * @return {Boolean} Returns true if one or more header values is set. 124 | */ 125 | has(_name) { 126 | return this._headers.has( 127 | normalizedName(_name) 128 | ) 129 | } 130 | 131 | /** 132 | * Return header value by name. If there is several headers returns all of them 133 | * concatenated by `, `. 134 | * 135 | * @example 136 | * 137 | * headers.set('accept', 'text/plain') 138 | * headers.get('accept') 139 | * // > "text/plain" 140 | * headers.append('accept', 'text/html') 141 | * headers.get('accept') 142 | * // > "text/plain, text/html" 143 | * 144 | * @param {String} _name Header name 145 | * @return {String} Concatenated header values. 146 | */ 147 | get(_name) { 148 | const name = normalizedName(_name) 149 | if (! this._headers.has(name)) { 150 | return '' 151 | } 152 | 153 | return this._headers.get(name).join(', ') 154 | } 155 | 156 | /** 157 | * Return iterator of header names. 158 | * 159 | * @return {Iterable.} Iterator of header names. 160 | */ 161 | keys() { 162 | return this._headers.keys() 163 | } 164 | 165 | /** 166 | * Return iterator of headers values. 167 | * 168 | * @return {Iterator.>} Iterator of each header values. 169 | */ 170 | values() { 171 | return Array.from(this._headers.values()) 172 | .map(function (value) { 173 | return value.join(', ') 174 | })[Symbol.iterator]() 175 | } 176 | 177 | /** 178 | * Returns iterator of entries. 179 | * 180 | * @return {Iterator.} Return iterator of Object.entries alike values. 181 | */ 182 | entries() { 183 | return Array.from(this._headers.entries()) 184 | .map(function ([name, value]) { 185 | return [name, value.join(', ')] 186 | })[Symbol.iterator]() 187 | } 188 | 189 | /** 190 | * Call `callback` for each header entry. 191 | * 192 | * @param {function(Array.,String)} fn Function that calls for each hander entry. 193 | * @param {type} thisArg This value for function. 194 | * @returns {void} Returns no value. 195 | */ 196 | forEach(fn, thisArg = this) { 197 | this._headers.forEach(function(values, key) { 198 | fn(values.join(', '), key, thisArg) 199 | }, thisArg) 200 | } 201 | 202 | /** 203 | * Throw TypeError with prevent changes message. 204 | * 205 | * @return {void} No return value. 206 | * @throws {TypeError} Everytime it's called. 207 | * @private 208 | */ 209 | wrongMode() { 210 | throw new TypeError(`Headers mode is ${this.mode}`) 211 | } 212 | 213 | /** 214 | * Not standard. Get raw header value as array of strings. Not concatenated 215 | * into string. If header not exists returns empty array. 216 | * 217 | * @param {String} name Header name. 218 | * @return {String[]} List of passed header values. 219 | */ 220 | raw(name) { 221 | if (this.has(name)) { 222 | return this._headers.get(name) 223 | } 224 | else { 225 | return [] 226 | } 227 | } 228 | } 229 | 230 | Headers.MODE_NONE = MODE_NONE 231 | Headers.MODE_IMMUTABLE = MODE_IMMUTABLE 232 | 233 | /** 234 | * Normalize HTTP Field name 235 | * 236 | * @param {*} _name HTTP Field name 237 | * @return {String} Returns normalized HTTP Field name 238 | * @throws {TypeError} If string contains unsupported characters 239 | */ 240 | function normalizedName(_name) { 241 | let name = _name 242 | 243 | if (typeof name !== 'string') { 244 | name = String(name) 245 | } 246 | 247 | if (/[^a-z0-9\-#$%&'*+.^_`|~\r\n]/i.test(name)) { 248 | throw new TypeError('Invalid character in header field name') 249 | } 250 | 251 | return name.toLowerCase() 252 | } 253 | 254 | /** 255 | * Normalize HTTP Field value. 256 | * 257 | * @param {*} _value Anything convertable to valid HTTP Field value string 258 | * @return {String} Normalized HTTP Field value. 259 | * @throws {TypeError} If value contains new line characters 260 | */ 261 | function normalizedValue(_value) { 262 | 263 | let value = _value 264 | 265 | if (typeof value !== 'string') { 266 | value = String(value) 267 | } 268 | 269 | if (/\r|\n/.test(value)) { 270 | throw new TypeError('Invalid newline character in header field value') 271 | } 272 | 273 | return value 274 | } 275 | 276 | module.exports = Headers 277 | -------------------------------------------------------------------------------- /packages/plant/src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./server') 2 | -------------------------------------------------------------------------------- /packages/plant/src/peer.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @class 4 | * @classdesc Peer represents the other side of connection. 5 | */ 6 | class Peer { 7 | /** 8 | * @param {object} options Peer options 9 | * @param {URI} options.uri Peer URI 10 | * @constructor 11 | */ 12 | constructor({uri}) { 13 | this.uri = uri 14 | } 15 | } 16 | 17 | module.exports = Peer 18 | -------------------------------------------------------------------------------- /packages/plant/src/request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Plant 3 | */ 4 | 5 | const isPlainObject = require('lodash.isplainobject') 6 | 7 | const {parseHeader, parseEntity} = require('./util/type-header') 8 | const {isReadableStream} = require('./util/stream') 9 | const {getMimeMatcher} = require('./util/mime-type-matcher') 10 | 11 | const Headers = require('./headers') 12 | 13 | /** 14 | * @typedef {Object} RequestOptions 15 | * @prop {String} method='GET' Request HTTP method. 16 | * @prop {URL} url - WebAPI URL object. 17 | * @prop {Headers|Object.} headers={} - Request headers. 18 | * @prop {ReadableStream|Null} body=null Request body. 19 | * @prop {Request} parent=null – Parent request. 20 | */ 21 | 22 | /** 23 | * @class 24 | * @classdesc Plant Request representation object 25 | * 26 | * @prop {String} method='GET' - Request method. 27 | * @prop {URL} url - WebAPI URL object. 28 | * @prop {Headers} headers - WebAPI request headers (in immmutable mode). 29 | * @prop {String[]} domains - Full domains of server splitted by dot `.`. 30 | * @prop {Buffer|Null} body - Request body as buffer. Null until received. 31 | * @prop {Request} parent=null – Parent request. 32 | */ 33 | class Request { 34 | /** 35 | * @param {RequestOptions} options Constructor options. 36 | * @constructor 37 | */ 38 | constructor({ 39 | method = 'get', 40 | headers = {}, 41 | url, 42 | body = null, 43 | parent = null, 44 | }) { 45 | this.url = url 46 | this.method = method.toUpperCase() 47 | this.headers = isPlainObject(headers) 48 | ? new Headers(headers, Headers.MODE_IMMUTABLE) 49 | : headers 50 | this.domains = /\.\d+$/.test(this.url.hostname) 51 | ? [] 52 | : this.url.hostname.split('.').reverse() 53 | 54 | if (body !== null && ! isReadableStream(body)) { 55 | throw new TypeError('options.body is not a readable stream') 56 | } 57 | 58 | if (parent !== null && parent instanceof Request === false) { 59 | throw new TypeError('options.parent should be instance of Request or null') 60 | } 61 | 62 | this.body = body 63 | this.bodyUsed = false 64 | this.buffer = null 65 | this.parent = parent 66 | } 67 | 68 | /** 69 | * Check if current request mime type in content-type header is equal `type`. 70 | * 71 | * @param {String} type List of mime types 72 | * @return {Boolean} Return true if content type header contains specified `types`. 73 | */ 74 | is(type) { 75 | const entity = parseEntity(this.headers.get('content-type') || '') 76 | 77 | return entity.type === type 78 | } 79 | 80 | /** 81 | * Get request content type from list of types 82 | * @param {String[]} types List of types to choose one. 83 | * @returns {String|Null} Return matched type or null if no type matched 84 | */ 85 | type(types) { 86 | const _types = normalizeTypes(types) 87 | const {type} = parseEntity(this.headers.get('content-type')) 88 | 89 | for (const {value, matcher} of _types) { 90 | if (matcher(type) === true) { 91 | return value 92 | } 93 | } 94 | 95 | return null 96 | } 97 | 98 | /** 99 | * Select which one of `types` contains in request's Accept header. 100 | * @param {String[]} types List of types to choose one. 101 | * @returns {String|Null} Return matched type or null if no type matched 102 | */ 103 | accept(types) { 104 | const _types = normalizeTypes(types) 105 | const cTypes = parseHeader(this.headers.get('accept')) 106 | .sort(function (a, b) { 107 | return (a.params.q - b.params.q) 108 | }) 109 | .map(function ({type}) { 110 | return type 111 | }) 112 | 113 | for (const cType of cTypes) { 114 | for (const {value, matcher} of _types) { 115 | if (matcher(cType) === true) { 116 | return value 117 | } 118 | } 119 | } 120 | 121 | return null 122 | } 123 | 124 | async text() { 125 | const contentType = this.headers.get('content-type') 126 | let encoding = 'utf8' 127 | if (contentType) { 128 | const charset = charsetFromContentType(contentType) 129 | 130 | if (charset) { 131 | encoding = charset 132 | } 133 | } 134 | 135 | const buffer = await this.arrayBuffer() 136 | const decoder = new TextDecoder(encoding) 137 | 138 | return decoder.decode(buffer).toString() 139 | } 140 | 141 | async arrayBuffer() { 142 | if (this.bodyUsed) { 143 | return this.buffer 144 | } 145 | 146 | const result = [] 147 | const reader = this.body.getReader() 148 | /* eslint-disable-next-line no-constant-condition */ 149 | while (true) { 150 | const {value, done} = await reader.read() 151 | if (done) { 152 | break 153 | } 154 | result.push(value) 155 | } 156 | reader.releaseLock() 157 | this.buffer = concatUint8Arrays(result) 158 | this.bodyUsed = true 159 | 160 | return this.buffer 161 | } 162 | 163 | json() { 164 | return this.text() 165 | .then(JSON.parse) 166 | } 167 | 168 | // blob() {} 169 | 170 | // formData() {} 171 | 172 | clone() { 173 | const copy = new this.constructor({ 174 | method: this.method, 175 | url: this.url, 176 | headers: this.headers, 177 | body: this.body, 178 | }) 179 | 180 | copy.buffer = this.buffer 181 | 182 | return copy 183 | } 184 | } 185 | 186 | // Naive request groups. 187 | // Usage is: aliases.json('application/json') // -> true 188 | const aliases = { 189 | json: getMimeMatcher(['application/json', 'application/json+*']), 190 | text: getMimeMatcher(['text/plain']), 191 | html: getMimeMatcher(['text/html', 'text/xhtml']), 192 | image: getMimeMatcher(['image/*']), 193 | } 194 | 195 | function normalizeTypes(types) { 196 | const result = [] 197 | 198 | for (const type of types) { 199 | if (type.includes('/')) { 200 | result.push({ 201 | value: type, 202 | matcher(value) { 203 | return value === type 204 | }, 205 | }) 206 | } 207 | else if (aliases.hasOwnProperty(type)) { 208 | result.push({ 209 | value: type, 210 | matcher(value) { 211 | return aliases[type](value) 212 | }, 213 | }) 214 | } 215 | } 216 | 217 | return result 218 | } 219 | 220 | function concatUint8Arrays(arrays) { 221 | let length = 0 222 | for (const array of arrays) { 223 | length += array.length 224 | } 225 | const result = new Uint8Array(length) 226 | let n = 0 227 | for (const array of arrays) { 228 | for (let i = 0; i < array.length; i++, n++) { 229 | result[n] = array[i] 230 | } 231 | } 232 | return result 233 | } 234 | 235 | function charsetFromContentType(contentType) { 236 | const parts = contentType.split(/;\s+/) 237 | 238 | if (parts.length < 2) { 239 | return null 240 | } 241 | 242 | const charset = parts[1] 243 | 244 | if (! charset.startsWith('charset=')) { 245 | return null 246 | } 247 | 248 | return charset.slice(8).trim() 249 | } 250 | 251 | module.exports = Request 252 | -------------------------------------------------------------------------------- /packages/plant/src/route.js: -------------------------------------------------------------------------------- 1 | class Route { 2 | static fromRequest(req) { 3 | return new this({ 4 | path: req.url.pathname, 5 | }) 6 | } 7 | 8 | constructor({ 9 | path = '/', 10 | basePath = '', 11 | params = {}, 12 | captured = [], 13 | } = {}) { 14 | this.path = path 15 | this.basePath = basePath 16 | this.params = Object.freeze(params) 17 | this.captured = Object.freeze(captured) 18 | } 19 | 20 | clone() { 21 | const copy = new this.constructor({ 22 | path: this.path, 23 | basePath: this.basePath, 24 | params: this.params, 25 | captured: this.captured, 26 | }) 27 | 28 | return copy 29 | } 30 | 31 | extend({ 32 | path = path, 33 | basePath = this.basePath, 34 | params = this.params, 35 | captured = this.captured, 36 | }) { 37 | this.path = path 38 | this.basePath = basePath 39 | this.params = Object.freeze(params) 40 | this.captured = Object.freeze(captured) 41 | return this 42 | } 43 | 44 | capture(path, params = {}) { 45 | path = path.replace(/\/$/, '') 46 | 47 | if (path[0] !== '/') { 48 | path = '/' + path 49 | } 50 | 51 | if (! this.path.startsWith(path)) { 52 | throw new Error('Current path does not start with provided path value') 53 | } 54 | else if (path.length > 1) { 55 | if (this.path.length !== path.length && this.path[path.length] !== '/') { 56 | throw new Error('Provided path has unexpected ending') 57 | } 58 | 59 | this.path = this.path.slice(path.length) 60 | this.basePath = this.basePath + path 61 | } 62 | 63 | this.params = Object.freeze({...this.params, ...params}) 64 | this.captured = Object.freeze([ 65 | ...this.captured, 66 | { 67 | path, 68 | params, 69 | }, 70 | ]) 71 | 72 | return this 73 | } 74 | } 75 | 76 | module.exports = Route 77 | -------------------------------------------------------------------------------- /packages/plant/src/socket.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Plant.Socket 3 | */ 4 | 5 | const EventEmitter = require('eventemitter3') 6 | 7 | const Peer = require('./peer') 8 | 9 | function noop() {} 10 | 11 | /** 12 | * @class 13 | * @name Socket 14 | * @classdesc Socket wraps connection object like a transform stream and provide 15 | * methods to manipulate socket state. 16 | * @prop {Boolean} isEnded Specify was socket ended or not. 17 | */ 18 | class Socket extends EventEmitter { 19 | /** 20 | * @param {Object} options Socket options. 21 | * @param {Function} [options.onEnd] Callback triggered when end event is emitted. 22 | * @param {function(Response):Promise} [options.onPush] Callback triggered when push requested by server. It should be set only when socket support pushes. 23 | * @constructor 24 | */ 25 | constructor({peer, onEnd = noop, onPush = null} = {}) { 26 | super() 27 | 28 | if (onPush !== null) { 29 | if (typeof onPush !== 'function') { 30 | throw new TypeError('options.onPush should be undefined, null or a function') 31 | } 32 | } 33 | 34 | if (peer instanceof Peer === false) { 35 | throw new TypeError('options.peer should be instance of a Peer') 36 | } 37 | 38 | this._peer = peer 39 | this._isEnded = false 40 | this._end = onEnd 41 | this._push = onPush 42 | } 43 | 44 | /** 45 | * get canPush - specify wether socket is available to push responses. 46 | * 47 | * @return {bool} true if constructor's options.onPush function is defined. 48 | */ 49 | get canPush() { 50 | return this._push !== null 51 | } 52 | 53 | /** 54 | * Tell if socket was aborted or ended by application. 55 | * 56 | * @return {Boolean} True if socket could not write. 57 | */ 58 | get isEnded() { 59 | return this._isEnded 60 | } 61 | 62 | /** 63 | * get peer - connection peer instance 64 | * 65 | * @return {Peer} Peer associated with the socket 66 | */ 67 | get peer() { 68 | return this._peer 69 | } 70 | 71 | /** 72 | * End socket and make it 73 | * 74 | * @return {void} No return value. 75 | */ 76 | end() { 77 | if (this._isEnded) { 78 | return 79 | } 80 | 81 | this._isEnded = true 82 | this._end() 83 | } 84 | 85 | destroy() { 86 | this.emit('destroy') 87 | } 88 | 89 | /** 90 | * push - Push response to the client 91 | * 92 | * @param {Response} response Plant Response instance 93 | * @return {Promise} Returns Promise resolved with sent response. 94 | */ 95 | push(response) { 96 | if (! this.canPush) { 97 | throw new Error('This socket could not push') 98 | } 99 | 100 | return this._push(response) 101 | .then(function() { 102 | return response 103 | }) 104 | } 105 | } 106 | 107 | module.exports = Socket 108 | -------------------------------------------------------------------------------- /packages/plant/src/uri.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @classdesc This is a URI object representation 4 | */ 5 | class URI { 6 | constructor(uri) { 7 | if (typeof uri === 'string') { 8 | throw new Error('URI parsing not implemented yet') 9 | } 10 | 11 | this.setParams(uri) 12 | } 13 | 14 | setParams({ 15 | protocol = '', 16 | username = '', 17 | password = '', 18 | hostname = '', 19 | port = '', 20 | pathname = '/', 21 | query = '', 22 | fragment = '', 23 | }) { 24 | this.protocol = protocol 25 | this.username = username 26 | this.password = password 27 | this.hostname = hostname 28 | this.port = port 29 | this.pathname = pathname 30 | this.query = query 31 | this.fragment = fragment 32 | 33 | return this 34 | } 35 | 36 | get host() { 37 | if (this.port) { 38 | return `${this.hostname}:${this.port}` 39 | } 40 | else { 41 | return `${this.hostname}` 42 | } 43 | } 44 | 45 | toString() { 46 | const parts = [] 47 | if (this.protocol) { 48 | parts.push(this.protocol) 49 | } 50 | parts.push(`//${this.host}`) 51 | parts.push(this.pathname) 52 | if (this.query.length) { 53 | parts.push(this.query) 54 | } 55 | if (this.fragment.length) { 56 | parts.push(this.fragment) 57 | } 58 | 59 | return parts.join('') 60 | } 61 | } 62 | 63 | module.exports = URI 64 | -------------------------------------------------------------------------------- /packages/plant/src/util/mime-type-matcher.js: -------------------------------------------------------------------------------- 1 | const escapeRegExp = require('lodash.escaperegexp') 2 | 3 | /** 4 | * getMimeMatcher - create mime type matcher function wich determines weather 5 | * passed `type` matches `types`, 6 | * 7 | * @param {Array} types List of types. 8 | * @return {function(String):Boolean} Type matcher function. 9 | */ 10 | function getMimeMatcher(types) { 11 | const matchers = types.map(function(type) { 12 | if (typeof type === 'string') { 13 | return stringMatcher(type) 14 | } 15 | else if (type instanceof RegExp) { 16 | return regExpMatcher(type) 17 | } 18 | else { 19 | throw new Error('Unknown type') 20 | } 21 | }) 22 | 23 | return function(value) { 24 | for (const matcher of matchers) { 25 | if (matcher(value)) { 26 | return true 27 | } 28 | } 29 | return false 30 | } 31 | } 32 | 33 | /** 34 | * stringMatcher - Return matcher function. If `origin` contains '*' it will 35 | * convert string to regexp and call regExpMatcher. Other way it will return 36 | * a function which check strings equality. 37 | * 38 | * @param {String} origin Mime type or mime type mask. 39 | * @return {function(String):Boolean} returns matcher function. 40 | */ 41 | function stringMatcher(origin) { 42 | if (origin.includes('*')) { 43 | return regExpMatcher(toRegExp(origin, '[^\/]+')) 44 | } 45 | 46 | return function(value) { 47 | return value === origin 48 | } 49 | } 50 | 51 | /** 52 | * regExpMatcher - return regexp matcher function. 53 | * 54 | * @param {RegExp} regexp Regular exression to match with. 55 | * @return {function(String):Boolean} Returns matcher function. 56 | */ 57 | function regExpMatcher(regexp) { 58 | return function(value) { 59 | return regexp.test(value) 60 | } 61 | } 62 | 63 | /** 64 | * toRegExp - Convert text `mask` to regular expression. Asterisk will be replaced 65 | * with `replacer`. 66 | * 67 | * @param {String} mask String containing asterisk. 68 | * @param {String} replacer Regular expression to substitute of asterisk. 69 | * @return {RegExp} Regular expression. 70 | */ 71 | function toRegExp(mask, replacer = '.+?') { 72 | const re = mask.split('*').map(escapeRegExp).join(replacer) 73 | 74 | return new RegExp('^' + re + '$') 75 | } 76 | 77 | exports.getMimeMatcher = getMimeMatcher 78 | -------------------------------------------------------------------------------- /packages/plant/src/util/stream.js: -------------------------------------------------------------------------------- 1 | const isObject = require('lodash.isobject') 2 | /** 3 | * isReadableStream - specify weather passed `value` has readable stream object 4 | * methods on, pipe and end. 5 | * 6 | * @param {*} value Value to check. 7 | * @return {Boolean} Return true if value is readable stream. 8 | */ 9 | function isReadableStream(value) { 10 | return isObject(value) 11 | && typeof value.getReader === 'function' 12 | } 13 | 14 | function isDisturbed(stream) { 15 | if (typeof stream._disturbed === 'boolean') { 16 | return stream._disturbed 17 | } 18 | else if (typeof Response !== 'undefined') { 19 | try { 20 | // eslint-disable-next-line no-undef 21 | const response = new Response(stream) 22 | // WebKit doesn't through 23 | return response.bodyUsed 24 | } 25 | catch (_) { 26 | return false 27 | } 28 | } 29 | else { 30 | throw new Error('Could not determine wether stream was disturbed') 31 | } 32 | } 33 | 34 | exports.isReadableStream = isReadableStream 35 | exports.isDisturbed = isDisturbed 36 | -------------------------------------------------------------------------------- /packages/plant/src/util/type-header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module TypeHeaderUtils 3 | */ 4 | 5 | /** 6 | * @typedef TypeEntity 7 | * @prop {String} type Type entry value 8 | * @prop {Object} params Type params, like `q`, `charset`, etc. 9 | */ 10 | 11 | /** 12 | * parseHeader - Parse complete type header value for Content-Type and Accept 13 | * headers. Example: 14 | * `text/html;q=1.0, image/png;q=0.1`. 15 | * 16 | * @param {string} header Request header. 17 | * @return {TypeEntity[]} Array of type objects. 18 | */ 19 | function parseHeader(header) { 20 | const entities = header.split(/\s*,\s*/) 21 | 22 | return entities.map(parseEntity) 23 | } 24 | 25 | /** 26 | * parseEntity - parse singe header type entry. Type entry is a string which 27 | * contains key value pairs separated with semicolon. Example: 28 | * `application/jsoncharset=utf8`. 29 | * 30 | * @param {String} entity Type entry. 31 | * @return {TypeEntity} returns type entry object. 32 | */ 33 | function parseEntity(entity) { 34 | const [type, ...tail] = entity.split(/;/) 35 | 36 | const params = getParams(tail) 37 | 38 | if (params.q) { 39 | params.q = parseFloat(params.q) 40 | } 41 | 42 | return {type, params} 43 | } 44 | 45 | /** 46 | * getParams - convert list of key-value strings into object with proper keys 47 | * and values. 48 | * 49 | * @param {String[]} params Array of key value strings. 50 | * @return {Object} Object. 51 | * @access private 52 | */ 53 | function getParams(params) { 54 | return params.map(function (param) { 55 | return param.split('=') 56 | }) 57 | .reduce(function (result, [name, value]) { 58 | return { 59 | ...result, 60 | [name]: value, 61 | } 62 | }, {}) 63 | } 64 | 65 | exports.parseHeader = parseHeader 66 | exports.parseEntity = parseEntity 67 | -------------------------------------------------------------------------------- /packages/plant/test/fetch.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | const should = require('should') 3 | 4 | const Plant = require('..') 5 | const {Response, Request} = Plant 6 | 7 | describe('fetch()', () => { 8 | it('should return response', 9 | async () => { 10 | const plant = new Plant() 11 | 12 | plant.use(({req, res}, next) => { 13 | if (req.url.pathname !== '/') { 14 | return next() 15 | } 16 | else { 17 | res.body = 'Subrequest' 18 | } 19 | }) 20 | 21 | plant.use(async ({req, res, fetch}) => { 22 | const subResponse = await fetch({ 23 | url: new URL('/', req.url), 24 | }) 25 | 26 | res.body = 'Hello, ' + subResponse.body + '!' 27 | }) 28 | 29 | const req = new Request({ 30 | url: new URL('http://localhost/test'), 31 | }) 32 | const res = new Response() 33 | 34 | await plant.getHandler()({req, res}) 35 | 36 | should(res.body).be.equal('Hello, Subrequest!') 37 | } 38 | ) 39 | 40 | it('should work with built-in router', 41 | async () => { 42 | const plant = new Plant() 43 | 44 | plant.use('/subrequest', ({res}) => { 45 | res.body = 'Subrequest' 46 | }) 47 | 48 | plant.use(async ({res, fetch}) => { 49 | const subResponse = await fetch({ 50 | url: '/subrequest', 51 | }) 52 | 53 | res.body = 'Hello, ' + subResponse.body + '!' 54 | }) 55 | 56 | const req = new Request({ 57 | url: new URL('http://localhost/'), 58 | }) 59 | const res = new Response() 60 | 61 | await plant.getHandler()({req, res}) 62 | 63 | should(res.body).be.equal('Hello, Subrequest!') 64 | } 65 | ) 66 | }) 67 | -------------------------------------------------------------------------------- /packages/plant/test/headers.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | const should = require('should') 3 | 4 | const {Headers} = require('..') 5 | 6 | describe('Headers()', function() { 7 | describe('Headers.Header()', function() { 8 | it('Should add headers from object', function() { 9 | const headers = new Headers({ 10 | prop: 'value', 11 | }) 12 | 13 | should(headers.has('prop')).be.True() 14 | should(headers.get('prop')).be.equal('value') 15 | }) 16 | 17 | it('Should add headers from entries', function() { 18 | const headers = new Headers([ 19 | ['entry', 'value'], 20 | ]) 21 | 22 | should(headers.has('entry')).be.True() 23 | should(headers.get('entry')).be.equal('value') 24 | }) 25 | }) 26 | 27 | describe('Headers.set()', function() { 28 | it('Should add new value', function() { 29 | const headers = new Headers() 30 | 31 | headers.set('type', 'test') 32 | should(headers.get('type')).be.equal('test') 33 | }) 34 | 35 | it('Should overwrite existing value', function() { 36 | const headers = new Headers({ 37 | type: 'test', 38 | }) 39 | 40 | headers.set('type', 'new value') 41 | should(headers.get('type')).be.equal('new value') 42 | }) 43 | }) 44 | 45 | describe('Headers.append()', function() { 46 | it('Should add new value', function() { 47 | const headers = new Headers() 48 | 49 | headers.append('type', 'test') 50 | should(headers.get('type')).be.equal('test') 51 | }) 52 | 53 | it('Should append to existing value', function() { 54 | const headers = new Headers({ 55 | type: 'value1', 56 | }) 57 | 58 | headers.append('type', 'value2') 59 | should(headers.get('type')).be.equal('value1, value2') 60 | }) 61 | }) 62 | 63 | describe('Headers.has()', function() { 64 | it('Should return `true` when header exists', function() { 65 | const headers = new Headers() 66 | 67 | headers.set('test', 'value') 68 | should(headers.has('test')).be.True() 69 | }) 70 | 71 | it('Should return `false` when header not exists', function() { 72 | const headers = new Headers() 73 | 74 | should(headers.has('test')).be.False() 75 | }) 76 | 77 | }) 78 | 79 | describe('Headers.delete()', function() { 80 | it('Should delete existing header', function() { 81 | const headers = new Headers({ 82 | test: 'value', 83 | }) 84 | 85 | should(headers.has('test')).be.True() 86 | 87 | headers.delete('test') 88 | 89 | should(headers.has('test')).be.False() 90 | }) 91 | }) 92 | 93 | describe('Headers.keys()', function() { 94 | it('Should return list of header names', function() { 95 | const headers = new Headers({ 96 | 'content-type': 'text/plain', 97 | 'content-length': '5', 98 | }) 99 | 100 | const list = Array.from(headers.keys()) 101 | 102 | should(list).be.deepEqual([ 103 | 'content-type', 104 | 'content-length', 105 | ]) 106 | }) 107 | }) 108 | 109 | describe('Headers.values()', function() { 110 | it('Should return list of header names', function() { 111 | const headers = new Headers({ 112 | 'content-type': 'text/plain', 113 | 'content-length': '5', 114 | }) 115 | 116 | const list = Array.from(headers.values()) 117 | 118 | should(list).be.deepEqual([ 119 | 'text/plain', 120 | '5', 121 | ]) 122 | }) 123 | 124 | it('Should return concatenated string for multiple header values', function() { 125 | const headers = new Headers() 126 | 127 | headers.append('accept-encoding', 'gzip') 128 | headers.append('accept-encoding', 'deflate') 129 | 130 | const list = Array.from(headers.values()) 131 | 132 | should(list).be.deepEqual(['gzip, deflate']) 133 | }) 134 | }) 135 | 136 | describe('Headers.entires()', function() { 137 | it('Should contain all headers', function() { 138 | const headers = new Headers({ 139 | 'content-type': 'text/plain', 140 | 'content-length': '5', 141 | }) 142 | 143 | const list = Array.from(headers.entries()) 144 | 145 | should(list).be.deepEqual([ 146 | ['content-type', 'text/plain'], 147 | ['content-length', '5'], 148 | ]) 149 | }) 150 | 151 | it('Should return concatenated string for multiple header values', function() { 152 | const headers = new Headers() 153 | 154 | headers.append('accept-encoding', 'gzip') 155 | headers.append('accept-encoding', 'deflate') 156 | 157 | const list = Array.from(headers.entries()) 158 | 159 | should(list).be.deepEqual([ 160 | ['accept-encoding', 'gzip, deflate'], 161 | ]) 162 | }) 163 | }) 164 | 165 | describe('Headers.forEach()', function() { 166 | it('Should iterate over stringified values', function() { 167 | const headers = new Headers({ 168 | 'content-type': 'text/plain', 169 | 'content-length': '5', 170 | }) 171 | 172 | const list = [] 173 | 174 | headers.forEach((value, key) => list.push([key, value])) 175 | 176 | should(list).be.deepEqual([ 177 | ['content-type', 'text/plain'], 178 | ['content-length', '5'], 179 | ]) 180 | }) 181 | }) 182 | 183 | describe('Headers.raw()', function() { 184 | it('Should return array for multiple header values', function() { 185 | const headers = new Headers() 186 | 187 | headers.append('accept-encoding', 'gzip') 188 | headers.append('accept-encoding', 'deflate') 189 | 190 | const list = headers.raw('accept-encoding') 191 | 192 | should(list).be.deepEqual(['gzip', 'deflate']) 193 | }) 194 | it('Should return empty array for missing header', function() { 195 | const headers = new Headers() 196 | 197 | const list = headers.raw('accept-encoding') 198 | 199 | should(list).be.deepEqual([]) 200 | }) 201 | }) 202 | }) 203 | -------------------------------------------------------------------------------- /packages/plant/test/index.js: -------------------------------------------------------------------------------- 1 | require('./fetch.spec') 2 | require('./headers.spec') 3 | require('./request.spec') 4 | require('./response.spec') 5 | require('./route.spec') 6 | require('./socket.spec') 7 | require('./server.spec') 8 | require('./uri.spec') 9 | -------------------------------------------------------------------------------- /packages/plant/test/request.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | 3 | const should = require('should') 4 | const {URL} = require('url') 5 | 6 | const {Request, Headers} = require('..') 7 | const {ReadableStream} = require('./utils/readable-stream') 8 | 9 | describe('Request()', function() { 10 | it('Should be a function', function() { 11 | should(Request).be.Function() 12 | }) 13 | 14 | describe('Request#is()', function() { 15 | it('Should use values from Request#headers', function() { 16 | const req = new Request({ 17 | url: new URL('http://localhost/'), 18 | headers: new Headers({ 19 | 'content-type': 'text/html', 20 | }), 21 | body: null, 22 | }) 23 | 24 | should(req.is('text/html')).be.equal(true) 25 | should(req.is('application/json')).be.equal(false) 26 | }) 27 | }) 28 | 29 | describe('Request#type()', function() { 30 | it('Should return "application/json" for "application/json" content type', function() { 31 | const req = new Request({ 32 | url: new URL('http://localhost/'), 33 | headers: new Headers({ 34 | 'content-type': 'application/json;charset=utf8', 35 | }), 36 | body: null, 37 | }) 38 | 39 | const type = req.type(['text/html', 'application/json']) 40 | 41 | should(type).be.a.String().and.be.equal('application/json') 42 | }) 43 | 44 | it('Should return "json" for "application/json" content type', function() { 45 | const req = new Request({ 46 | url: new URL('http://localhost/'), 47 | headers: new Headers({ 48 | 'content-type': 'application/json;charset=utf8', 49 | }), 50 | body: null, 51 | }) 52 | 53 | const type = req.type(['text/html', 'json']) 54 | 55 | should(type).be.a.String().and.be.equal('json') 56 | }) 57 | 58 | it('Should return `null` for not an "application/json" content type', function() { 59 | const req = new Request({ 60 | url: new URL('http://localhost/'), 61 | headers: new Headers({ 62 | 'content-type': 'application/json', 63 | }), 64 | body: null, 65 | }) 66 | 67 | const type = req.type(['html', 'video']) 68 | 69 | should(type).be.equal(null) 70 | }) 71 | 72 | ;[ 73 | 'image/gif', 74 | 'image/png', 75 | 'image/jpg', 76 | 'image/jpeg', 77 | ] 78 | .map(function (type) { 79 | it('Should return "image" for "' + type + '" content type', function() { 80 | const req = new Request({ 81 | url: new URL('http://localhost/'), 82 | headers: new Headers({ 83 | 'content-type': type, 84 | }), 85 | body: null, 86 | }) 87 | 88 | const result = req.type(['image']) 89 | 90 | should(result).be.a.String().and.be.equal('image') 91 | }) 92 | }) 93 | }) 94 | 95 | describe('Request#accept()', function() { 96 | it('Should return "json" for "application/json" accept value', function() { 97 | const req = new Request({ 98 | url: new URL('http://localhost/'), 99 | headers: new Headers({ 100 | 'accept': 'application/json', 101 | }), 102 | body: null, 103 | }) 104 | 105 | const type = req.accept(['html', 'json']) 106 | 107 | should(type).be.a.String().and.be.equal('json') 108 | }) 109 | 110 | it('Should return `null` for not an "application/json" accept type', function() { 111 | const req = new Request({ 112 | url: new URL('http://localhost/'), 113 | headers: new Headers({ 114 | 'accept': 'application/json', 115 | }), 116 | }) 117 | 118 | const type = req.accept(['html', 'video']) 119 | 120 | should(type).be.equal(null) 121 | }) 122 | }) 123 | 124 | describe('Request#text()', function() { 125 | it('Should receive data from readable stream', function() { 126 | const req = new Request({ 127 | url: new URL('http://localhost/'), 128 | body: new ReadableStream([ 129 | Buffer.from('Hello', 'utf8'), 130 | ]), 131 | }) 132 | 133 | return req.text() 134 | .then((text) => { 135 | should(text).be.equal('Hello') 136 | }) 137 | }) 138 | }) 139 | 140 | describe('Request#json()', function() { 141 | it('Should receive Object from readable stream', function() { 142 | const req = new Request({ 143 | url: new URL('http://localhost/'), 144 | body: new ReadableStream([ 145 | Buffer.from('[{"value": true}]', 'utf8'), 146 | ]), 147 | }) 148 | 149 | return req.json() 150 | .then((json) => { 151 | should(json).be.Array().and.be.deepEqual([{value: true}]) 152 | }) 153 | }) 154 | }) 155 | }) 156 | -------------------------------------------------------------------------------- /packages/plant/test/response.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | const should = require('should') 3 | 4 | const {Response, Request} = require('..') 5 | 6 | describe('Response()', function() { 7 | it('Should be a function', function() { 8 | should(Response).be.a.Function() 9 | }) 10 | 11 | describe('Response.json()', function() { 12 | it('Should convert value to json string', function() { 13 | const res = new Response() 14 | 15 | res.json({a: 1}) 16 | 17 | should(res.body).be.a.String().and.equal('{"a":1}') 18 | }) 19 | 20 | it('Should set proper content-length header', function() { 21 | const res = new Response() 22 | 23 | res.json({a: 1}) 24 | 25 | should(res.headers.get('content-length')).be.equal('7') 26 | }) 27 | 28 | it('Should set content-type header to "application/json"', function() { 29 | const res = new Response() 30 | 31 | res.json({a: 1}) 32 | 33 | should(res.headers.get('content-type')).be.equal('application/json') 34 | }) 35 | }) 36 | 37 | describe('Response.text()', function() { 38 | it('Should return same string', function() { 39 | const res = new Response() 40 | 41 | res.text('Hello, World!') 42 | 43 | should(res.body).be.a.String().and.equal('Hello, World!') 44 | }) 45 | 46 | it('Should set proper content-length header', function() { 47 | const res = new Response() 48 | 49 | res.text('Hello, World!') 50 | 51 | should(res.headers.get('content-length')).be.equal('13') 52 | }) 53 | 54 | it('Should set content-type header to "text/plain"', function() { 55 | const res = new Response() 56 | 57 | res.text('Hello, World!') 58 | 59 | should(res.headers.get('content-type')).be.equal('text/plain') 60 | }) 61 | }) 62 | 63 | describe('Response.html()', function() { 64 | it('Should return same string', function() { 65 | const res = new Response() 66 | 67 | res.html('') 68 | 69 | should(res.body).be.a.String().and.equal('') 70 | }) 71 | 72 | it('Should set proper content-length header', function() { 73 | const res = new Response() 74 | 75 | res.html('') 76 | 77 | should(res.headers.get('content-length')).be.equal('7') 78 | }) 79 | 80 | it('Should set content-type header to "text/html"', function() { 81 | const res = new Response() 82 | 83 | res.html('') 84 | 85 | should(res.headers.get('content-type')).be.equal('text/html') 86 | }) 87 | }) 88 | 89 | describe('Response.stream()', function() { 90 | class ReadableStreamMock { 91 | constructor() { 92 | this._disturbed = false 93 | } 94 | getReader() {} 95 | } 96 | 97 | it('Should append stream as body', function() { 98 | const res = new Response() 99 | const stream = new ReadableStreamMock() 100 | res.stream(stream) 101 | 102 | should(res.body).be.an.Object().and.equal(stream) 103 | }) 104 | 105 | it('Should throw if stream is disturbed', function() { 106 | const res = new Response() 107 | const stream = new ReadableStreamMock() 108 | stream._disturbed = true 109 | 110 | should.throws(() => res.stream(stream)) 111 | }) 112 | }) 113 | 114 | describe('Response.body', function() { 115 | it('Should accept string', function() { 116 | const res = new Response() 117 | 118 | res.body = 'Hello' 119 | 120 | should(res.body).be.a.String().and.equal('Hello') 121 | }) 122 | 123 | it('Should set proper content-length header with ascii string', function() { 124 | const res = new Response() 125 | 126 | res.body = 'Hello' 127 | 128 | should(res.headers.get('content-length')).be.equal('5') 129 | }) 130 | 131 | it('Should set proper content-length header with utf-8 string', function() { 132 | const res = new Response() 133 | 134 | res.body = 'Zю' 135 | 136 | should(res.headers.get('content-length')).be.equal('3') 137 | }) 138 | 139 | it('Should set proper content-length header with cp1251 string', function() { 140 | const res = new Response() 141 | 142 | res.headers.set('content-type', 'text/plain; charset=cp1251') 143 | res.body = 'Zю' 144 | 145 | should(res.headers.get('content-length')).be.equal('3') 146 | }) 147 | 148 | it('Should accept Byffer', function() { 149 | const res = new Response() 150 | 151 | res.body = Buffer.from('Hello') 152 | 153 | should(res.body).be.instanceof(Buffer) 154 | should(res.body.toString('utf8')).be.equal('Hello') 155 | }) 156 | 157 | it('Should set proper content-length header', function() { 158 | const res = new Response() 159 | 160 | res.body = Buffer.from('Hello') 161 | 162 | should(res.headers.get('content-length')).be.equal('5') 163 | }) 164 | }) 165 | 166 | describe('Response#statusText', function() { 167 | it('Should return "OK" by default', function() { 168 | const res = new Response() 169 | 170 | should(res.statusText).be.equal('OK') 171 | }) 172 | 173 | it('Should return "Not Found" for 404', function() { 174 | const res = new Response({ 175 | status: 404, 176 | }) 177 | 178 | should(res.statusText).be.equal('Not Found') 179 | }) 180 | 181 | it('Should return "Internal Server Error" for 500', function() { 182 | const res = new Response({ 183 | status: 500, 184 | }) 185 | 186 | should(res.statusText).be.equal('Internal Server Error') 187 | }) 188 | }) 189 | 190 | describe('Response#redirected', function() { 191 | it('Should be `true` if status is 301', function() { 192 | const res = new Response({ 193 | status: 301, 194 | }) 195 | 196 | should(res.redirected).be.equal(true) 197 | }) 198 | 199 | it('Should be `false` if status is 200', function() { 200 | const res = new Response({ 201 | status: 200, 202 | }) 203 | 204 | should(res.redirected).be.equal(false) 205 | }) 206 | 207 | it('Should be `false` if status is 0', function() { 208 | const res = new Response({ 209 | status: 0, 210 | }) 211 | 212 | should(res.redirected).be.equal(false) 213 | }) 214 | }) 215 | 216 | describe('Response#push()', function() { 217 | it('Should accept Request as target', function() { 218 | const res = new Response({ 219 | url: new URL('http://localhost/test'), 220 | }) 221 | 222 | should(res.pushes.length).be.equal(0) 223 | 224 | res.push(new Request({ 225 | url: res.url, 226 | })) 227 | 228 | should(res.pushes.length).be.equal(1) 229 | }) 230 | 231 | it('Should accept Response as target', function() { 232 | const res = new Response({ 233 | url: new URL('http://localhost/test'), 234 | }) 235 | 236 | should(res.pushes.length).be.equal(0) 237 | 238 | res.push(new Response({ 239 | url: res.url, 240 | status: 200, 241 | body: '', 242 | })) 243 | 244 | should(res.pushes.length).be.equal(1) 245 | }) 246 | 247 | it('Should accept URL as target', function() { 248 | const res = new Response({ 249 | url: new URL('http://localhost/test'), 250 | }) 251 | 252 | should(res.pushes.length).be.equal(0) 253 | 254 | res.push(new URL('/page', res.url)) 255 | 256 | should(res.pushes.length).be.equal(1) 257 | }) 258 | 259 | it('Should accept string as target', function() { 260 | const res = new Response({ 261 | url: new URL('http://localhost/test'), 262 | }) 263 | 264 | should(res.pushes.length).be.equal(0) 265 | 266 | res.push('/page') 267 | 268 | should(res.pushes.length).be.equal(1) 269 | }) 270 | 271 | it('Should throw otherwise', function() { 272 | const res = new Response({ 273 | url: new URL('http://localhost/test'), 274 | }) 275 | 276 | should.throws(() => res.push(null)) 277 | }) 278 | }) 279 | }) 280 | -------------------------------------------------------------------------------- /packages/plant/test/route.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | const should = require('should') 3 | 4 | const {Route} = require('..') 5 | 6 | describe('Route()', () => { 7 | describe('#capture()', () => { 8 | it('Should cut path from Route#path', () => { 9 | const route = new Route({ 10 | path: '/user/1/album/1', 11 | }) 12 | 13 | route.capture('/user/1') 14 | 15 | should(route.path).be.equal('/album/1') 16 | }) 17 | 18 | it('Should normalize path', () => { 19 | const route = new Route({ 20 | path: '/user/1/album/1', 21 | }) 22 | 23 | route.capture('user/1/') 24 | 25 | should(route.path).be.equal('/album/1') 26 | }) 27 | 28 | it('Should append path to Route#basePath', () => { 29 | const route = new Route({ 30 | path: '/user/1/album/1', 31 | }) 32 | 33 | route.capture('/user/1') 34 | 35 | should(route.basePath).be.equal('/user/1') 36 | }) 37 | 38 | it('Should extend Route#params', () => { 39 | const route = new Route({ 40 | path: '/user/1/album/1', 41 | }) 42 | 43 | route.capture('/user/1', {id: 1}) 44 | 45 | should(route.params).be.deepEqual({id: 1}) 46 | }) 47 | 48 | it('Should append captured path to Route#captured', () => { 49 | const route = new Route({ 50 | path: '/user/1/album/1', 51 | }) 52 | 53 | route.capture('/user/1', {id: 1}) 54 | 55 | should(route.captured.length).be.equal(1) 56 | should(route.captured[0]).be.an.Object 57 | 58 | should(route.captured[0]).has.ownProperty('path') 59 | .which.is.equal('/user/1') 60 | 61 | should(route.captured[0]).has.ownProperty('params') 62 | .which.is.deepEqual({id: 1}) 63 | }) 64 | 65 | it('Should capture path till the end', () => { 66 | const route = new Route({ 67 | path: '/user/1/album/1', 68 | }) 69 | 70 | route.capture('/user/1', {userId: 1}) 71 | route.capture('/album/1', {albumId: 1}) 72 | 73 | should(route.path).be.equal('') 74 | should(route.basePath).be.equal('/user/1/album/1') 75 | should(route.params).be.deepEqual({ 76 | userId: 1, 77 | albumId: 1, 78 | }) 79 | should(route.captured.length).be.equal(2) 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /packages/plant/test/socket.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | const should = require('should') 3 | 4 | const {Socket, Peer, URI} = require('..') 5 | 6 | describe('Socket()', function() { 7 | it('Should be a function', function() { 8 | should(Socket).be.a.Function() 9 | }) 10 | 11 | describe('Socket.end()', function() { 12 | it('Should set isEnded `true`', function() { 13 | const socket = new Socket({ 14 | peer: new Peer({ 15 | uri: new URI({ 16 | protocol: 'process:', 17 | hostname: process.pid, 18 | }), 19 | }), 20 | }) 21 | 22 | should(socket.isEnded).be.False() 23 | socket.end() 24 | should(socket.isEnded).be.True() 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /packages/plant/test/uri.spec.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | const should = require('should') 3 | 4 | const {URI} = require('..') 5 | 6 | describe('URI()', function() { 7 | it('Should be a function', function() { 8 | should(URI).be.a.Function() 9 | }) 10 | 11 | describe('URI.toString()', function() { 12 | it('Should convert string with protocol, hostname and port', function() { 13 | const uri = new URI({ 14 | protocol: 'tcp:', 15 | hostname: '127.0.0.1', 16 | port: '12345', 17 | }) 18 | 19 | should(uri.toString()).be.equal('tcp://127.0.0.1:12345/') 20 | }) 21 | 22 | it('Should convert string with protocol and hostname', function() { 23 | const uri = new URI({ 24 | protocol: 'tcp:', 25 | hostname: '127.0.0.1', 26 | }) 27 | 28 | should(uri.toString()).be.equal('tcp://127.0.0.1/') 29 | }) 30 | 31 | it('Should convert string with hostname and port', function() { 32 | const uri = new URI({ 33 | hostname: '127.0.0.1', 34 | port: '12345', 35 | }) 36 | 37 | should(uri.toString()).be.equal('//127.0.0.1:12345/') 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /packages/plant/test/utils/readable-stream.js: -------------------------------------------------------------------------------- 1 | class ReadableStreamMock { 2 | constructor(values) { 3 | this.values = values 4 | this.reader = null 5 | } 6 | 7 | get locked() { 8 | return this.reader !== null 9 | } 10 | 11 | getReader() { 12 | if (this.locked) { 13 | throw new Error('Stream is locked') 14 | } 15 | 16 | const values = this.values.slice() 17 | this.reader = { 18 | read: async () => { 19 | if (values.length) { 20 | let value = values.shift() 21 | 22 | if (typeof value === 'string') { 23 | value = new TextEncoder('utf8').encode(value) 24 | } 25 | 26 | return { 27 | value, 28 | done: false, 29 | } 30 | } 31 | else { 32 | this.reader = null 33 | return { 34 | value: void 0, 35 | done: true, 36 | } 37 | } 38 | }, 39 | cancel: () => { 40 | this.cancel() 41 | }, 42 | releaseLock: () => { 43 | this.reader = null 44 | values.splice(0, values.length) 45 | }, 46 | } 47 | 48 | return this.reader 49 | } 50 | 51 | pipeTo() {} 52 | pipeThrough() {} 53 | tee() {} 54 | 55 | cancel() { 56 | if (this.reader) { 57 | this.reader.releaseLock() 58 | } 59 | } 60 | } 61 | 62 | exports.ReadableStream = ReadableStreamMock 63 | -------------------------------------------------------------------------------- /packages/router/.npmignore: -------------------------------------------------------------------------------- 1 | ../-/.npmignore -------------------------------------------------------------------------------- /packages/router/license: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Copyright (c) 2017-2019 Rumkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/router", 3 | "version": "1.2.0", 4 | "description": "Router for plant server", 5 | "repository": { 6 | "type": "git", 7 | "url": "github.com/rumkin/plant" 8 | }, 9 | "homepage": "https://github.com/rumkin/plant/tree/master/packages/router", 10 | "author": "Rumkin (https://rumk.in/)", 11 | "main": "src/index.js", 12 | "engines": { 13 | "node": ">=6" 14 | }, 15 | "scripts": { 16 | "build": "npm run clean && mkdir dist && npm run compile && npm run minify", 17 | "clean": "rm -rf dist", 18 | "compile": "browserify -s PlantRouter src/index.js > dist/router.js", 19 | "lint": "npm run lint:src && npm run lint:test", 20 | "lint:src": "eslint src/**.js", 21 | "lint:test": "eslint test/**.js", 22 | "minify": "babel-minify dist/router.js -o dist/router.min.js", 23 | "prepublishOnly": "allow-publish-tag next && npm run lint && npm test && npm run build", 24 | "test": "mocha test/*.spec.js" 25 | }, 26 | "dependencies": { 27 | "@plant/flow": "^1.0.0", 28 | "lodash.isstring": "^4.0.1", 29 | "path-to-regexp": "^1.7.0" 30 | }, 31 | "devDependencies": { 32 | "@plant/plant": "^2.0.0-rc9", 33 | "allow-publish-tag": "^2.0.0", 34 | "babel-minify": "^0.5.0", 35 | "mocha": "^6.1.4", 36 | "should": "^13.2.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/router/readme.md: -------------------------------------------------------------------------------- 1 | # Plant/Router 2 | 3 | Router for Plant server. 4 | 5 | ## Table of Contents 6 | 7 | * [Install](#install). 8 | * [Usage](#usage). 9 | * [API](#api). 10 | * [Router](#router-type). 11 | 12 | ## Install 13 | 14 | ```shell 15 | npm i @plant/router 16 | ``` 17 | 18 | ## Usage 19 | 20 | Strict route example: 21 | 22 | ```javascript 23 | const Router = require('@plant/router') 24 | 25 | const router = new Router() 26 | 27 | router.get('/users/:id', ({res, route}) => { 28 | // Get params 29 | route.params.id // -> {id: "1"} 30 | // Get current path segment 31 | route.path // '/users/1' 32 | // Path before router 33 | route.basePath // '/' 34 | }) 35 | ``` 36 | 37 | Wildcard route with nested router example: 38 | 39 | ```javascript 40 | const Router = require('@plant/router') 41 | 42 | const router = new Router() 43 | 44 | router.use('/posts/*', Router.create((router) => { 45 | router.get('/:id', async () => { 46 | // ... 47 | }) 48 | 49 | router.post('/', async () => { 50 | // ... 51 | }) 52 | })) 53 | ``` 54 | 55 | ## API 56 | 57 | ### Router Type 58 | 59 | Router allows to filtrate request by HTTP method and URL pattern and to extract 60 | values from the URL. 61 | 62 | ##### Example 63 | 64 | ```javascript 65 | const Router = require('@plant/router') 66 | 67 | const router = new Router() 68 | 69 | router.get('/users/:id', () => { /* get resource */ }) 70 | router.post('/users', () => { /* post resource */ }) 71 | router.delete('/users/:id', () => { /* delete resource */ }) 72 | ``` 73 | 74 | ### `Router.Router()` 75 | ``` 76 | () -> Router 77 | ``` 78 | 79 | Router constructor has no arguments. 80 | 81 | ### `Router.create()` 82 | ``` 83 | (create: (router: Router) -> void) -> Router 84 | ``` 85 | 86 | Factory method. Accepts `create` method as argument which is a function 87 | with router configuration logic. Example: 88 | 89 | ```javascript 90 | Router.create((router) => { 91 | router.get('/greet', ({res}) => res.text('Hello World')) 92 | }) 93 | ``` 94 | 95 | ### `Router#use()` 96 | 97 | ```text 98 | (route:String, ...handlers:Handle) -> Router 99 | ``` 100 | 101 | Method to add handler for any HTTP method. 102 | 103 | ### `Router#before()` 104 | 105 | ```text 106 | (...handlers:Handle) -> Router 107 | ``` 108 | 109 | Add handlers which will be fired before each handler defined by `.use`, `.get`, 110 | and other route handlers when route params are match route params 111 | (route and method). 112 | 113 | It can be helpful when you need to prepend each route with expensive 114 | computations which should be done only if request matches defined options. It 115 | could be user loading or disk reading. 116 | 117 | ### `Router#get()` 118 | 119 | ```text 120 | (route:String, ...handlers:Handle) -> Router 121 | ``` 122 | 123 | Specify `route` `handlers` for `GET` HTTP method. 124 | 125 | ##### Example 126 | 127 | ```javascript 128 | // Strict route 129 | router.get('/users/:id', () => {}) 130 | // Wildcard route 131 | router.get('/posts/*', () => {}) 132 | ``` 133 | 134 | ### `Router#post()` 135 | 136 | Same as [Router#get()](#routerget) but for `POST` HTTP method. 137 | 138 | ### `Router#put()` 139 | 140 | Same as [Router#get()](#routerget) but for `PUT` HTTP method. 141 | 142 | ### `Router#patch()` 143 | 144 | Same as [Router#get()](#routerget) but for `PATCH` HTTP method. 145 | 146 | ### `Router#delete()` 147 | 148 | Same as [Router#get()](#routerget) but for `DELETE` HTTP method. 149 | 150 | ### `Router#head()` 151 | 152 | Same as [Router#get()](#routerget) but for `HEAD` HTTP method. 153 | 154 | ### `Router#options()` 155 | 156 | Same as [Router#get()](#routerget) but for `OPTIONS` HTTP method. 157 | 158 | ### `Router#addRoute()` 159 | ``` 160 | (method:string|string[], route:string, ...handler:Handle) -> Router 161 | ``` 162 | 163 | Add route handler and return router instance. Use `method` to specify HTTP 164 | method or methods supported by the `route` `handler`. 165 | 166 | ## License 167 | 168 | MIT © [Rumkin](https://rumk.in) 169 | -------------------------------------------------------------------------------- /packages/test-http/.npmignore: -------------------------------------------------------------------------------- 1 | ../-/.npmignore -------------------------------------------------------------------------------- /packages/test-http/.testuprc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": "console" 3 | } 4 | -------------------------------------------------------------------------------- /packages/test-http/license: -------------------------------------------------------------------------------- 1 | This software is released under the MIT license: 2 | 3 | Copyright (c) 2017-2019 Rumkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/test-http/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/test-server-suite", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@testup/cli": { 8 | "version": "0.4.0", 9 | "resolved": "https://registry.npmjs.org/@testup/cli/-/cli-0.4.0.tgz", 10 | "integrity": "sha512-SXJf9jAz+b2eQ103mClGRS0DtthQVK2Ff4YzPd//Rv9J7nfVgPbHvJieUzIeCHAoIxd9qOYMhAMp3mNru5uL6g==", 11 | "dev": true, 12 | "requires": { 13 | "coa": "^2.0.2" 14 | } 15 | }, 16 | "@testup/console-reporter": { 17 | "version": "0.1.1", 18 | "resolved": "https://registry.npmjs.org/@testup/console-reporter/-/console-reporter-0.1.1.tgz", 19 | "integrity": "sha512-+mnpuzbzMJSlNcHtuJthDBJSa1tsWjXTeGwRsRo3QNkNvkp5NOdJxqUNT1SPFjdFHcAe7mfO2TXO4QWYnTkrDQ==", 20 | "dev": true 21 | }, 22 | "@testup/core": { 23 | "version": "0.1.2", 24 | "resolved": "https://registry.npmjs.org/@testup/core/-/core-0.1.2.tgz", 25 | "integrity": "sha512-/IfKbu399VoqCQY9suEbU18+3Uxyn/14uVrg8MBt+NO+3EnyOtd/ZPCt2mMWGcvPiXcT8W1QhssSg53gd2x0WQ==", 26 | "dev": true 27 | }, 28 | "@types/q": { 29 | "version": "1.5.2", 30 | "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", 31 | "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==", 32 | "dev": true 33 | }, 34 | "ansi-styles": { 35 | "version": "3.2.1", 36 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 37 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 38 | "dev": true, 39 | "requires": { 40 | "color-convert": "^1.9.0" 41 | } 42 | }, 43 | "chalk": { 44 | "version": "2.4.2", 45 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 46 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 47 | "dev": true, 48 | "requires": { 49 | "ansi-styles": "^3.2.1", 50 | "escape-string-regexp": "^1.0.5", 51 | "supports-color": "^5.3.0" 52 | } 53 | }, 54 | "coa": { 55 | "version": "2.0.2", 56 | "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", 57 | "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", 58 | "dev": true, 59 | "requires": { 60 | "@types/q": "^1.5.1", 61 | "chalk": "^2.4.1", 62 | "q": "^1.1.2" 63 | } 64 | }, 65 | "color-convert": { 66 | "version": "1.9.3", 67 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 68 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 69 | "dev": true, 70 | "requires": { 71 | "color-name": "1.1.3" 72 | } 73 | }, 74 | "color-name": { 75 | "version": "1.1.3", 76 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 77 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 78 | "dev": true 79 | }, 80 | "escape-string-regexp": { 81 | "version": "1.0.5", 82 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 83 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 84 | "dev": true 85 | }, 86 | "has-flag": { 87 | "version": "3.0.0", 88 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 89 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 90 | "dev": true 91 | }, 92 | "q": { 93 | "version": "1.5.1", 94 | "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", 95 | "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", 96 | "dev": true 97 | }, 98 | "should": { 99 | "version": "13.2.3", 100 | "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", 101 | "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", 102 | "dev": true, 103 | "requires": { 104 | "should-equal": "^2.0.0", 105 | "should-format": "^3.0.3", 106 | "should-type": "^1.4.0", 107 | "should-type-adaptors": "^1.0.1", 108 | "should-util": "^1.0.0" 109 | } 110 | }, 111 | "should-equal": { 112 | "version": "2.0.0", 113 | "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", 114 | "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", 115 | "dev": true, 116 | "requires": { 117 | "should-type": "^1.4.0" 118 | } 119 | }, 120 | "should-format": { 121 | "version": "3.0.3", 122 | "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", 123 | "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=", 124 | "dev": true, 125 | "requires": { 126 | "should-type": "^1.3.0", 127 | "should-type-adaptors": "^1.0.1" 128 | } 129 | }, 130 | "should-type": { 131 | "version": "1.4.0", 132 | "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", 133 | "integrity": "sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM=", 134 | "dev": true 135 | }, 136 | "should-type-adaptors": { 137 | "version": "1.1.0", 138 | "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", 139 | "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", 140 | "dev": true, 141 | "requires": { 142 | "should-type": "^1.3.0", 143 | "should-util": "^1.0.0" 144 | } 145 | }, 146 | "should-util": { 147 | "version": "1.0.0", 148 | "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz", 149 | "integrity": "sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=", 150 | "dev": true 151 | }, 152 | "supports-color": { 153 | "version": "5.5.0", 154 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 155 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 156 | "dev": true, 157 | "requires": { 158 | "has-flag": "^3.0.0" 159 | } 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /packages/test-http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/test-http", 3 | "version": "0.5.3", 4 | "description": "Simple Node.js native HTTP test server", 5 | "keywords": [ 6 | "http", 7 | "native-http", 8 | "test", 9 | "test-utils" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "github.com/rumkin/plant" 14 | }, 15 | "homepage": "https://github.com/rumkin/plant/tree/master/packages/test-http", 16 | "author": "Rumkin (https://rumk.in/)", 17 | "main": "src/index.js", 18 | "engines": { 19 | "node": ">=11" 20 | }, 21 | "scripts": { 22 | "lint": "npm run lint:src && npm run lint:test", 23 | "lint:src": "eslint src/**.js", 24 | "lint:test": "eslint test/**.js", 25 | "prepublishOnly": "npm test", 26 | "test": "testup run test/http.spec.js test/https.spec.js test/http2.spec.js test/https2.spec.js" 27 | }, 28 | "license": "MIT", 29 | "devDependencies": { 30 | "@testup/cli": "^0.4.0", 31 | "@testup/console-reporter": "^0.1.1", 32 | "@testup/core": "^0.1.2", 33 | "should": "^13.2.3" 34 | }, 35 | "publishConfig": { 36 | "access": "public" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/test-http/readme.md: -------------------------------------------------------------------------------- 1 | # Plant Test HTTP Suite 2 | 3 | This is a very simple test servers made for Plant development. 4 | 5 | ## Install 6 | 7 | ```bash 8 | npm i @plant/test-http 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```javascript 14 | const {createHttp} = require('@plant/test-http') 15 | // or 16 | const createHttp = require('@plant/test-http/http') 17 | 18 | const server = createHttp((req, res) => { 19 | res.end('Hello') 20 | }) 21 | 22 | server.listen(0) 23 | 24 | server.fetch('/index.html') 25 | .then(({status, headers, body, text}) => { 26 | body.toString('utf8') === text; // true 27 | }) 28 | ``` 29 | 30 | ## API 31 | 32 | * `createHttp()` – create HTTP test server 33 | * `createHttp2()` – create HTTP2 test server 34 | * `createHttps()` – create HTTPS test server 35 | * `createHttps2()` – create HTTPS2 test server (the same as http2, but with HTTP2 test options) 36 | * `fetchHttp()` – fetch HTTP resource using promisified API. 37 | * `fetchHttp2()` – fetch HTTP2 resource using promisified API. 38 | * `fetchHttps()` – fetch HTTPS resource using promisified API. 39 | 40 | ## Copyright 41 | 42 | MIT © [Rumkin](https://rumk.in) 43 | -------------------------------------------------------------------------------- /packages/test-http/src/fetch-http.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | 3 | function fetch(url, {body, ...options} = {}) { 4 | return new Promise(function (resolve, reject) { 5 | 6 | const req = http.request(url + '', options) 7 | 8 | req.on('response', (res) => { 9 | let chunks = [] 10 | 11 | res.on('data', (chunk) => { 12 | chunks.push(chunk) 13 | }) 14 | 15 | res.on('end', () => { 16 | resolve({ 17 | url, 18 | status: res.statusCode, 19 | headers: res.headers, 20 | body: Buffer.concat(chunks), 21 | get text() { 22 | return this.body.toString('utf8') 23 | }, 24 | get json() { 25 | return JSON.parse(this.text) 26 | }, 27 | }) 28 | }) 29 | 30 | res.on('error', reject) 31 | }) 32 | 33 | req.on('error', reject) 34 | 35 | if (body) { 36 | req.write(body) 37 | } 38 | 39 | req.end() 40 | }) 41 | } 42 | 43 | module.exports = fetch 44 | -------------------------------------------------------------------------------- /packages/test-http/src/fetch-http2.js: -------------------------------------------------------------------------------- 1 | const http2 = require('http2') 2 | 3 | function fetch(url, {body, headers = {}, ...options} = {}) { 4 | return new Promise(function (resolve, reject) { 5 | let chunks = [] 6 | const response = { 7 | headers: {} 8 | } 9 | const pushed = [] 10 | 11 | const client = http2.connect( 12 | new URL('/', url), options 13 | ) 14 | 15 | client.on('error', reject) 16 | 17 | client.on('stream', (pushedStream, requestHeaders) => { 18 | let pushChunks = [] 19 | const push = { 20 | url: new URL(requestHeaders[':path'], url), 21 | get text() { 22 | return this.body.toString('utf8') 23 | }, 24 | get json() { 25 | return JSON.parse(this.text) 26 | }, 27 | } 28 | pushedStream.on('push', (responseHeaders) => { 29 | push.status = parseInt(responseHeaders[':status'], 10) 30 | push.headers = {...requestHeaders, ...responseHeaders} 31 | }) 32 | pushedStream.on('data', (chunk) => { 33 | pushChunks.push(chunk) 34 | }) 35 | pushedStream.on('end', () => { 36 | push.body = Buffer.concat(pushChunks).toString('utf8') 37 | pushed.push(push) 38 | }) 39 | }) 40 | 41 | const req = client.request({ 42 | ':path': url.pathname + url.search, 43 | ...headers, 44 | }, { 45 | endStream: !!body ? false : true, 46 | }) 47 | 48 | req.on('response', (_headers) => { 49 | response.headers = {..._headers} 50 | }) 51 | 52 | req.on('data', (chunk) => { 53 | chunks.push(chunk) 54 | }) 55 | 56 | req.on('end', () => { 57 | resolve({ 58 | url, 59 | status: parseInt(response.headers[':status'], 10), 60 | headers: response.headers, 61 | body: Buffer.concat(chunks), 62 | get text() { 63 | return this.body.toString('utf8') 64 | }, 65 | get json() { 66 | return JSON.parse(this.text) 67 | }, 68 | pushed, 69 | }) 70 | client.close() 71 | }) 72 | 73 | req.on('error', reject) 74 | if (body) { 75 | req.write(body) 76 | } 77 | req.end() 78 | }) 79 | } 80 | 81 | module.exports = fetch 82 | -------------------------------------------------------------------------------- /packages/test-http/src/fetch-https.js: -------------------------------------------------------------------------------- 1 | const https = require('https') 2 | 3 | function fetch(url, {body, ...options} = {}) { 4 | return new Promise(function (resolve, reject) { 5 | 6 | const req = https.request(url + '', options) 7 | 8 | req.on('response', (res) => { 9 | let chunks = [] 10 | 11 | res.on('data', (chunk) => { 12 | chunks.push(chunk) 13 | }) 14 | 15 | res.on('end', () => { 16 | resolve({ 17 | url, 18 | status: res.statusCode, 19 | headers: res.headers, 20 | body: Buffer.concat(chunks), 21 | get text() { 22 | return this.body.toString('utf8') 23 | }, 24 | get json() { 25 | return JSON.parse(this.text) 26 | }, 27 | }) 28 | }) 29 | 30 | res.on('error', reject) 31 | }) 32 | 33 | req.on('error', reject) 34 | 35 | if (body) { 36 | req.write(body) 37 | } 38 | 39 | req.end() 40 | }) 41 | } 42 | 43 | module.exports = fetch 44 | -------------------------------------------------------------------------------- /packages/test-http/src/http.js: -------------------------------------------------------------------------------- 1 | /* global URL */ 2 | 3 | const http = require('http') 4 | const fetch = require('./fetch-http') 5 | 6 | function createHttp(handler, options) { 7 | const server = http.createServer(options, handler) 8 | 9 | server.fetch = function(url, requesOptions, host = '127.0.0.1') { 10 | const address = this.address() 11 | 12 | return fetch( 13 | new URL(url, new URL(`http://${host}:${address.port}/}`)), 14 | requesOptions 15 | ) 16 | } 17 | 18 | return server 19 | } 20 | 21 | module.exports = createHttp 22 | -------------------------------------------------------------------------------- /packages/test-http/src/http2.js: -------------------------------------------------------------------------------- 1 | /* global URL */ 2 | 3 | const http2 = require('http2') 4 | const fetch = require('./fetch-http2') 5 | 6 | function createHttp2(handler, options) { 7 | const server = http2.createServer(options, handler) 8 | 9 | server.fetch = function(url, requesOptions, host = '127.0.0.1') { 10 | const address = this.address() 11 | 12 | return fetch( 13 | new URL(url, new URL(`http://${host}:${address.port}/}`)), 14 | requesOptions 15 | ) 16 | } 17 | 18 | return server 19 | } 20 | 21 | module.exports = createHttp2 22 | -------------------------------------------------------------------------------- /packages/test-http/src/https.js: -------------------------------------------------------------------------------- 1 | /* global URL */ 2 | 3 | const https = require('https') 4 | const fetch = require('./fetch-https') 5 | 6 | function createHttps(handler, options) { 7 | const server = https.createServer(options, handler) 8 | 9 | server.fetch = function(url, requesOptions, host = 'localhost') { 10 | const address = this.address() 11 | 12 | return fetch( 13 | new URL(url, new URL(`https://${host}:${address.port}/}`)), 14 | { 15 | rejectUnauthorized: false, 16 | ...requesOptions, 17 | } 18 | ) 19 | } 20 | 21 | return server 22 | } 23 | 24 | module.exports = createHttps 25 | -------------------------------------------------------------------------------- /packages/test-http/src/https2.js: -------------------------------------------------------------------------------- 1 | /* global URL */ 2 | 3 | const http2 = require('http2') 4 | const fetch = require('./fetch-http2') 5 | 6 | function createHttps2(handler, options) { 7 | const server = http2.createSecureServer(options, handler) 8 | 9 | server.fetch = function(url, requesOptions, host = 'localhost') { 10 | const address = this.address() 11 | 12 | return fetch( 13 | new URL(url, new URL(`https://${host}:${address.port}/}`)), 14 | { 15 | rejectUnauthorized: false, 16 | ...requesOptions, 17 | } 18 | ) 19 | } 20 | 21 | return server 22 | } 23 | 24 | module.exports = createHttps2 25 | -------------------------------------------------------------------------------- /packages/test-http/src/index.js: -------------------------------------------------------------------------------- 1 | exports.createHttp = require('./http') 2 | exports.createHttps = require('./https') 3 | exports.createHttp2 = require('./http2') 4 | exports.createHttps2 = require('./https2') 5 | exports.fetchHttp = require('./fetch-http') 6 | exports.fetchHttp2 = require('./fetch-http2') 7 | exports.fetchHttps = require('./fetch-https') 8 | -------------------------------------------------------------------------------- /packages/test-http/test/http.spec.js: -------------------------------------------------------------------------------- 1 | const should = require('should') 2 | 3 | const createServer = require('../http') 4 | 5 | module.exports = ({describe, it}) => { 6 | describe('HTTP', () => { 7 | it('Should handle request', async () => { 8 | const server = createServer((req, res) => { 9 | res.end('Hello, World') 10 | }) 11 | 12 | server.listen(0) 13 | 14 | try { 15 | const res = await server.fetch('/index.html') 16 | should(res.status).be.equal(200) 17 | should(res.text).be.equal('Hello, World') 18 | should(res.url.pathname).be.equal('/index.html') 19 | } 20 | finally { 21 | server.close() 22 | } 23 | }) 24 | 25 | it('Should parse JSON', async () => { 26 | const server = createServer((req, res) => { 27 | res.end(JSON.stringify({hello: 'world'})) 28 | }) 29 | 30 | server.listen(0) 31 | 32 | try { 33 | const {status, json} = await server.fetch('/') 34 | should(status).be.equal(200) 35 | should(json).be.deepEqual({hello: 'world'}) 36 | } 37 | finally { 38 | server.close() 39 | } 40 | }) 41 | 42 | it('Should receive correct URL', async () => { 43 | const server = createServer((req, res) => { 44 | res.end(req.url + '') 45 | }) 46 | 47 | server.listen(0) 48 | 49 | try { 50 | const {status, text} = await server.fetch('/page?test=true') 51 | should(status).be.equal(200) 52 | should(text).be.equal('/page?test=true') 53 | } 54 | finally { 55 | server.close() 56 | } 57 | }) 58 | 59 | it('Should send body', async () => { 60 | const server = createServer((req, res) => { 61 | req.pipe(res) 62 | }) 63 | 64 | server.listen(0) 65 | 66 | try { 67 | const {status, text} = await server.fetch('/', { 68 | method: 'POST', 69 | body: 'Hello', 70 | }) 71 | should(status).be.equal(200) 72 | should(text).be.equal('Hello') 73 | } 74 | finally { 75 | server.close() 76 | } 77 | }) 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /packages/test-http/test/http2.spec.js: -------------------------------------------------------------------------------- 1 | const should = require('should') 2 | 3 | const createServer = require('../http2') 4 | 5 | module.exports = ({describe, it}) => { 6 | describe('HTTP2', () => { 7 | it('Should handle request', async () => { 8 | const server = createServer((req, res) => { 9 | res.end('Hello, World') 10 | }) 11 | 12 | server.listen(0) 13 | 14 | try { 15 | const res = await server.fetch('/index.html') 16 | should(res.status).be.equal(200) 17 | should(res.text).be.equal('Hello, World') 18 | should(res.url.pathname).be.equal('/index.html') 19 | } 20 | finally { 21 | server.close() 22 | } 23 | }) 24 | 25 | it('Should parse JSON', async () => { 26 | const server = createServer((req, res) => { 27 | res.end(JSON.stringify({hello: 'world'})) 28 | }) 29 | 30 | server.listen(0) 31 | 32 | try { 33 | const {status, json} = await server.fetch('/') 34 | should(status).be.equal(200) 35 | should(json).be.deepEqual({hello: 'world'}) 36 | } 37 | finally { 38 | server.close() 39 | } 40 | }) 41 | 42 | it('Should receive correct URL', async () => { 43 | const server = createServer((req, res) => { 44 | res.end(req.url + '') 45 | }) 46 | 47 | server.listen(0) 48 | 49 | try { 50 | const {status, text} = await server.fetch('/page?test=true') 51 | should(status).be.equal(200) 52 | should(text).be.equal('/page?test=true') 53 | } 54 | finally { 55 | server.close() 56 | } 57 | }) 58 | 59 | it('Should send body', async () => { 60 | const server = createServer((req, res) => { 61 | req.pipe(res) 62 | }) 63 | 64 | server.listen(0) 65 | 66 | try { 67 | const {status, text} = await server.fetch('/', { 68 | method: 'POST', 69 | body: 'Hello', 70 | }) 71 | should(status).be.equal(200) 72 | should(text).be.equal('Hello') 73 | } 74 | finally { 75 | server.close() 76 | } 77 | }) 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /packages/test-http/test/https.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const should = require('should') 3 | 4 | const createServer = require('../https') 5 | 6 | const ssl = { 7 | key: fs.readFileSync(__dirname + '/ssl/key.pem'), 8 | cert: fs.readFileSync(__dirname + '/ssl/cert.pem'), 9 | } 10 | 11 | module.exports = ({describe, it}) => { 12 | describe('HTTPS', () => { 13 | it('Should handle request', async () => { 14 | const server = createServer((req, res) => { 15 | res.end('Hello, World') 16 | }, ssl) 17 | 18 | server.listen(0) 19 | 20 | try { 21 | const res = await server.fetch('/index.html') 22 | should(res.status).be.equal(200) 23 | should(res.text).be.equal('Hello, World') 24 | should(res.url.pathname).be.equal('/index.html') 25 | } 26 | finally { 27 | server.close() 28 | } 29 | }) 30 | 31 | it('Should parse JSON', async () => { 32 | const server = createServer((req, res) => { 33 | res.end(JSON.stringify({hello: 'world'})) 34 | }, ssl) 35 | 36 | server.listen(0) 37 | 38 | try { 39 | const {status, json} = await server.fetch('/') 40 | should(status).be.equal(200) 41 | should(json).be.deepEqual({hello: 'world'}) 42 | } 43 | finally { 44 | server.close() 45 | } 46 | }) 47 | 48 | it('Should receive correct URL', async () => { 49 | const server = createServer((req, res) => { 50 | res.end(req.url + '') 51 | }, ssl) 52 | 53 | server.listen(0) 54 | 55 | try { 56 | const {status, text} = await server.fetch('/page?test=true') 57 | should(status).be.equal(200) 58 | should(text).be.equal('/page?test=true') 59 | } 60 | finally { 61 | server.close() 62 | } 63 | }) 64 | 65 | it('Should send body', async () => { 66 | const server = createServer((req, res) => { 67 | req.pipe(res) 68 | }, ssl) 69 | 70 | server.listen(0) 71 | 72 | try { 73 | const {status, text} = await server.fetch('/', { 74 | method: 'POST', 75 | body: 'Hello', 76 | }) 77 | should(status).be.equal(200) 78 | should(text).be.equal('Hello') 79 | } 80 | finally { 81 | server.close() 82 | } 83 | }) 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /packages/test-http/test/https2.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const should = require('should') 3 | 4 | const createServer = require('../https2') 5 | 6 | const ssl = { 7 | key: fs.readFileSync(__dirname + '/ssl/key.pem'), 8 | cert: fs.readFileSync(__dirname + '/ssl/cert.pem'), 9 | } 10 | 11 | module.exports = ({describe, it}) => { 12 | describe('HTTPS2', () => { 13 | it('Should handle request', async () => { 14 | const server = createServer((req, res) => { 15 | res.end('Hello, World') 16 | }, ssl) 17 | 18 | server.listen(0) 19 | 20 | try { 21 | const res = await server.fetch('/index.html') 22 | should(res.status).be.equal(200) 23 | should(res.text).be.equal('Hello, World') 24 | should(res.url.pathname).be.equal('/index.html') 25 | } 26 | finally { 27 | server.close() 28 | } 29 | }) 30 | 31 | it('Should parse JSON', async () => { 32 | const server = createServer((req, res) => { 33 | res.end(JSON.stringify({hello: 'world'})) 34 | }, ssl) 35 | 36 | server.listen(0) 37 | 38 | try { 39 | const {status, json} = await server.fetch('/') 40 | should(status).be.equal(200) 41 | should(json).be.deepEqual({hello: 'world'}) 42 | } 43 | finally { 44 | server.close() 45 | } 46 | }) 47 | 48 | it('Should receive correct URL', async () => { 49 | const server = createServer((req, res) => { 50 | res.end(req.url + '') 51 | }, ssl) 52 | 53 | server.listen(0) 54 | 55 | try { 56 | const {status, text} = await server.fetch('/page?test=true') 57 | should(status).be.equal(200) 58 | should(text).be.equal('/page?test=true') 59 | } 60 | finally { 61 | server.close() 62 | } 63 | }) 64 | 65 | it('Should send body', async () => { 66 | const server = createServer((req, res) => { 67 | req.pipe(res) 68 | }, ssl) 69 | 70 | server.listen(0) 71 | 72 | try { 73 | const {status, text} = await server.fetch('/', { 74 | method: 'POST', 75 | body: 'Hello', 76 | }) 77 | should(status).be.equal(200) 78 | should(text).be.equal('Hello') 79 | } 80 | finally { 81 | server.close() 82 | } 83 | }) 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /packages/test-http/test/ssl/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICvjCCAaYCCQDQB+GpXnGY5DANBgkqhkiG9w0BAQsFADAhMQ4wDAYDVQQKDAVQ 3 | bGFudDEPMA0GA1UECwwGU2VydmVyMB4XDTE5MDQxOTE2MTYxMVoXDTIwMDQxODE2 4 | MTYxMVowITEOMAwGA1UECgwFUGxhbnQxDzANBgNVBAsMBlNlcnZlcjCCASIwDQYJ 5 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKhwRr4P+7N6yR/Ff/e/wy8bNW04B945 6 | Sh/WO1+m6e42BD/KlqxsG8xybWxyv/ntYwJ06GSEMePVVb09hWZLTstRv96BUjFv 7 | 8Ul5PmypRgw9IURLzIRIcpNa68FGrt+uhrPS9baj3Jzh7iNbfcA0Ba/hy0MDbLph 8 | by5tSShyQA/VYWaX/dv2NBdjVfES2nBIqT3k0D7md2mD/qvuSrRi6h9t4sId73SU 9 | E0k9OXi4Pq1DcDvXBqFAi/FG7Tp7KkPnav9CusSbQsJA5W1jWgNcXTA5vSrKStNb 10 | /jPoWwRBfmz9TVbpECdaxygDs0SCQTyN3sxKI6PvT6uGkSB3dVoBWqMCAwEAATAN 11 | BgkqhkiG9w0BAQsFAAOCAQEADiBtZgjbob4ztyFymSN+JMsvRgJsQPvHeNKwf026 12 | 0mSNLwGW4CuB54M8MW8y42+Qr233CTPGUixvWoMPTgdMPrbwK5JWt9wWw1Pw/Hni 13 | klwV85CSq/r6FUrjDkjXtb+fitQxVcUUMSb/h3u3sFQIlBBNUivR+7CHKmc4pJIC 14 | VKpV1LBlP29Q9hDmFEyKKpkPwHgoaQfqSnaDDGES3CjQUn+DEgJQl6nLldJWRiRU 15 | rKk1nkODiaXBewIpXhv1sQUK3oVoZs/aUv//BypvQ7etvYHuvCdr2lgRD8CSwtk2 16 | 1CxShOOZbA11mgNupixT21OwtbAvo4m+VyGR9DPFpaA/RA== 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /packages/test-http/test/ssl/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCocEa+D/uzeskf 3 | xX/3v8MvGzVtOAfeOUof1jtfpunuNgQ/ypasbBvMcm1scr/57WMCdOhkhDHj1VW9 4 | PYVmS07LUb/egVIxb/FJeT5sqUYMPSFES8yESHKTWuvBRq7froaz0vW2o9yc4e4j 5 | W33ANAWv4ctDA2y6YW8ubUkockAP1WFml/3b9jQXY1XxEtpwSKk95NA+5ndpg/6r 6 | 7kq0YuofbeLCHe90lBNJPTl4uD6tQ3A71wahQIvxRu06eypD52r/QrrEm0LCQOVt 7 | Y1oDXF0wOb0qykrTW/4z6FsEQX5s/U1W6RAnWscoA7NEgkE8jd7MSiOj70+rhpEg 8 | d3VaAVqjAgMBAAECggEAXhj7LEq5jnbVzQ4Eg195puNIYY+ftaHDqy1/Vdxla1J5 9 | 5TlEG2b50KlMP/2LChB383NkMGM5i9IuZ93qnE8N4b/1tFQCmuOypB07pnCaVVQB 10 | NaoywuPGPlPYyMy3/PX/Ao6j/jhkkrAU3WPLSIjHdI5rgzBymVy9Q+6BpDrPVwgx 11 | jiy2A0a1jXGz068dgwBoV0qp0TELrdHIk/RLDbEtzWUD8bhHwZjkdh8SAYnVrUMw 12 | vg/82ANCD3o7b4Ok/5L4t5m7F0R/ADg9XNABaiCfMXz2UvtbAg4UxffaTDYuN92g 13 | W2Rz19lWokk1nX0bVE3A/tGPiKoYZB4BelivMVnywQKBgQDW/zoelLOsIc6KflhU 14 | nhlTXg+PHtM2A40OrsvHI/yBIklJDB2F0mVH02M7dMeRgP1mNJfIK57y06HHXtdl 15 | 17xCuxgDdnUH/HJIMDcIUac2NmjMtNrRrsAHF6IRj4pbT3YVFj0L1qMvlo41xsfn 16 | pNc81k5GZO50JhmE7wxeb5ekOQKBgQDIj+8OzDgmGESZSkPQZkRmkuzhY3XeM0ZL 17 | 8Ep9D7aSndxnNTPy+dxpaHUpyFbhbOA31S1vdgzREAWtKN5UqfrvzpF0iU59Makn 18 | FzWsU6I8ac2HRQ/Y7UdsWyT5WjKMGLf31TzKI0JifFftfOz+/cO9s7xGOd+mpPVN 19 | InCWsRGNuwKBgDdG89h8/x0YrBPrnCZVZ8mJe5KeqEtQ6mmGA5q14+wHtrPzS3vm 20 | tmebL/5Pbig48+3dQ9ERdhKU2xl5hwQGTb8Sf4AUas6c1307+EpJRCaqIpPPRBt5 21 | RKIOL3s4XqhPa9rMFvH+Q4KuwO2OqEMknLpll0Z+GNkAGruVAqcdJe3xAoGANBR8 22 | JUGOiwXeOlf4iBMmS+R3MofbQZna9Tkufo8n/6aSZxJ/rOaI/64qTnFBbkQRbS4k 23 | ID9tUJRyhOaJ5T5GdSMUzkghY40TuZzjSR5mkH2A61FZriDfXRnF3iI34f1BOE/c 24 | +zhwspZLVtYLzKMkwwv7Jdk9ZE6NjDwXNGpCfqUCgYBWztPYaqomkt7eIsU1FtUQ 25 | ztGaqLJWxtaZ0kT97B0rD4sNxEZVEIdC8PQi1LnoRH0mUmqENK4rqzAvN4nM6LcR 26 | MycsFJ/hs+NutZOrKjmKmN8jM9PSj/RvT/UzGoyghZFwGSlF/so+gyDlHD2m/Uln 27 | mFVipKTjLWSBU5+kXJ72PQ== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /packages/vfs/.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": ["jsdoc"] 5 | }, 6 | "source": { 7 | "include": ["src", "package.json", "README.md"], 8 | "includePattern": ".js$", 9 | "excludePattern": "(node_modules/|test/|docs)" 10 | }, 11 | "plugins": [ 12 | "plugins/markdown" 13 | ], 14 | "templates": { 15 | "cleverLinks": false, 16 | "monospaceLinks": true, 17 | "useLongnameInNav": false, 18 | "showInheritedInNav": true 19 | }, 20 | "opts": { 21 | "destination": "./tmp/docs/", 22 | "encoding": "utf8", 23 | "private": true, 24 | "recurse": true, 25 | "template": "./node_modules/minami" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/vfs/.npmignore: -------------------------------------------------------------------------------- 1 | ../-/.npmignore -------------------------------------------------------------------------------- /packages/vfs/license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Rumkin (rumk.in) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/vfs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@plant/vfs", 3 | "version": "0.3.1", 4 | "description": "Virtual FS handler for Plant server", 5 | "main": "src/index.js", 6 | "engines": { 7 | "node": ">=8.0" 8 | }, 9 | "scripts": { 10 | "lint": "npm run lint:src && npm run lint:test", 11 | "lint:src": "eslint src/**.js", 12 | "lint:test": "eslint test/**.js", 13 | "prepublishOnly": "allow-publish-tag next && npm run lint", 14 | "test": "testup run -r console test/**.spec.js" 15 | }, 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@plant/node-stream-utils": "^0.1.2", 19 | "@plant/plant": "^2.4.0", 20 | "@testup/cli": "^0.4.0", 21 | "@testup/console-reporter": "^0.1.1", 22 | "@testup/core": "^0.2.1", 23 | "allow-publish-tag": "^2.1.1", 24 | "memfs": "^2.16.1", 25 | "pify": "^4.0.1" 26 | }, 27 | "dependencies": { 28 | "escape-html": "^1.0.3", 29 | "mime": "^2.4.4" 30 | }, 31 | "publishConfig": { 32 | "access": "public" 33 | }, 34 | "directories": { 35 | "test": "test" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/rumkin/plant.git" 40 | }, 41 | "keywords": [ 42 | "@plant/plant", 43 | "web", 44 | "http", 45 | "handler" 46 | ], 47 | "author": "rumkin", 48 | "homepage": "https://github.com/rumkin/plant/tree/master/packages/vfs", 49 | "bugs": "https://github.com/rumkin/plant/issues" 50 | } 51 | -------------------------------------------------------------------------------- /packages/vfs/readme.md: -------------------------------------------------------------------------------- 1 | # VFS 2 | 3 | VFS module is a set of factories which creates directory and file web handlers 4 | This factories require abstract FS to be provided. 5 | 6 | ## License 7 | 8 | MIT © [Rumkin](https://rumk.in) 9 | -------------------------------------------------------------------------------- /packages/vfs/test/index.spec.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | 3 | const {Request, Response, Route} = require('@plant/plant') 4 | 5 | const { 6 | createDirHandler, 7 | createFileHandler, 8 | } = require('..') 9 | 10 | const {createFs, readStream} = require('./lib/fs') 11 | 12 | function createCtx({ 13 | url = new URL('http://localhost/'), 14 | } = {}) { 15 | const req = new Request({ 16 | url, 17 | }) 18 | const res = new Response({ 19 | url, 20 | }) 21 | const route = new Route({ 22 | path: url.pathname, 23 | }) 24 | 25 | return {req, res, route} 26 | } 27 | 28 | module.exports = ({describe, it}) => { 29 | describe('createDirHandler()', () => { 30 | it('Should serve file', async () => { 31 | const ctx = createCtx({ 32 | url: new URL('http://localhost/index.html'), 33 | }) 34 | 35 | const vfs = createFs() 36 | const file = { 37 | name: '/index.html', 38 | content: 'Hello', 39 | } 40 | await vfs.writeFile(file.name, file.content) 41 | 42 | const handle = createDirHandler(vfs, '/') 43 | 44 | await handle(ctx) 45 | const {res} = ctx 46 | 47 | assert.equal(res.headers.get('content-type'), 'text/html', 'Content-type header is text/html') 48 | 49 | const body = await readStream(res.body, 'utf8') 50 | 51 | assert.equal(body, file.content, 'Response body matches') 52 | }) 53 | 54 | it('Should serve file', async () => { 55 | const ctx = createCtx({ 56 | url: new URL('http://localhost/dir/index.html'), 57 | }) 58 | 59 | const vfs = createFs() 60 | const file = { 61 | name: '/index.html', 62 | content: 'Hello', 63 | } 64 | await vfs.writeFile(file.name, file.content) 65 | 66 | const handle = createDirHandler(vfs, '/') 67 | 68 | await handle(ctx) 69 | const {res} = ctx 70 | 71 | assert.equal(res.hasBody, false, 'No content returned') 72 | }) 73 | 74 | it('Should serve nested file', async () => { 75 | const ctx = createCtx({ 76 | url: new URL('http://localhost/dir/index.txt'), 77 | }) 78 | 79 | const vfs = createFs() 80 | const fileA = { 81 | name: '/dir/index.txt', 82 | content: 'a', 83 | } 84 | const fileB = { 85 | name: '/index.txt', 86 | content: 'b', 87 | } 88 | await vfs.mkdir('/dir') 89 | await vfs.writeFile(fileA.name, fileA.content) 90 | await vfs.writeFile(fileB.name, fileB.content) 91 | 92 | const handle = createDirHandler(vfs, '/') 93 | 94 | await handle(ctx) 95 | const {res} = ctx 96 | 97 | assert.equal(res.headers.get('content-type'), 'text/plain', 'Content-type header is text/plain') 98 | 99 | const body = await readStream(res.body, 'utf8') 100 | 101 | assert.equal(body, fileA.content, 'Response body matches') 102 | }) 103 | 104 | it('Should use options.indexFile', async () => { 105 | const ctx = createCtx({ 106 | url: new URL('http://localhost/'), 107 | }) 108 | 109 | const vfs = createFs() 110 | const file = { 111 | name: '/test.html', 112 | content: 'Hello', 113 | } 114 | await vfs.writeFile(file.name, file.content) 115 | 116 | const handle = createDirHandler(vfs, '/', { 117 | indexFile: '/test.html', 118 | }) 119 | 120 | await handle(ctx) 121 | const {res} = ctx 122 | 123 | assert.equal(res.headers.get('content-type'), 'text/html', 'Content-type header is text/html') 124 | 125 | const body = await readStream(res.body, 'utf8') 126 | 127 | assert.equal(body, file.content, 'Response body matches') 128 | }) 129 | 130 | it('Should serve nothing', async () => { 131 | const ctx = createCtx({ 132 | url: new URL('http://localhost/'), 133 | }) 134 | 135 | const vfs = createFs() 136 | const file = { 137 | name: '/test.html', 138 | content: 'Hello', 139 | } 140 | await vfs.writeFile(file.name, file.content) 141 | 142 | const handle = createDirHandler(vfs, '/') 143 | 144 | await handle(ctx) 145 | const {res} = ctx 146 | 147 | assert.equal(res.hasBody, false, 'No body was set') 148 | }) 149 | }) 150 | 151 | describe('createFileHandler()', () => { 152 | it('Should serve file', async () => { 153 | const ctx = createCtx({ 154 | url: new URL('http://localhost/index.txt'), 155 | }) 156 | 157 | const vfs = createFs() 158 | const file = { 159 | name: '/index.txt', 160 | content: 'Hello', 161 | } 162 | await vfs.writeFile(file.name, file.content) 163 | 164 | const handle = createFileHandler(vfs, '/index.txt') 165 | 166 | await handle(ctx) 167 | const {res} = ctx 168 | 169 | assert.equal(res.headers.get('content-type'), 'text/plain', 'Content-type header is text/plain') 170 | 171 | const body = await readStream(res.body, 'utf8') 172 | assert.equal(body, file.content, 'File content matches') 173 | }) 174 | }) 175 | } 176 | -------------------------------------------------------------------------------- /packages/vfs/test/lib/fs.js: -------------------------------------------------------------------------------- 1 | const {Volume} = require('memfs') 2 | const pify = require('pify') 3 | 4 | const {NodeToWebStream} = require('@plant/node-stream-utils') 5 | 6 | function createFs() { 7 | const fs = new Volume() 8 | 9 | return { 10 | exists: pify((...args) => fs.exists(...args)), 11 | stat: pify((...args) => fs.stat(...args)), 12 | lstat: pify((...args) => fs.lstat(...args)), 13 | mkdir: pify((...args) => fs.mkdir(...args)), 14 | readDir: pify((...args) => fs.readDir(...args)), 15 | readLink: pify((...args) => fs.readLink(...args)), 16 | writeFile: pify((...args) => fs.writeFile(...args)), 17 | readFile: pify((...args) => fs.readFile(...args)), 18 | createReadStream: (...args) => { 19 | return new NodeToWebStream(fs.createReadStream(...args)) 20 | }, 21 | } 22 | } 23 | 24 | async function readStream(stream, encoding) { 25 | const chunks = [] 26 | const reader = stream.getReader() 27 | 28 | for await (const chunk of reader) { 29 | chunks.push(chunk) 30 | } 31 | 32 | const buffer = Buffer.concat(chunks) 33 | 34 | if (encoding !== void 0) { 35 | return buffer.toString(encoding) 36 | } 37 | else { 38 | return buffer 39 | } 40 | } 41 | 42 | exports.createFs = createFs 43 | exports.readStream = readStream 44 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | Plant logo 3 |

4 | 5 | # Plant 6 | 7 | [![npm](https://img.shields.io/npm/v/@plant/plant.svg?style=flat-square)](https://npmjs.com/package/@plant/plant) 8 | [![npm](https://img.shields.io/npm/dw/@plant/plant.svg?style=flat-square)](https://npmjs.com/package/@plant/plant) 9 | ![](https://img.shields.io/badge/size-8KiB-blue.svg?style=flat-square) 10 | 11 | [NPM](https://npmjs.com/package/@plant/plant) · 12 | [Source](packages/plant) · [Readme](packages/plant/readme.md) 13 | 14 | Plant is WebAPI standards based HTTP2 web server, created with 15 | modular architecture and functional design in mind. Also it's pure and less coupled. 16 | 17 | Plant supports HTTP 1 and HTTP 2 protocols. But it's transport agnostic and can work right 18 | in the browser over WebSockets, WebRTC, or PostMessage. 19 | 20 | ## Features 21 | 22 | - ☁️ Lightweight: only **8** KiB minified and gzipped. 23 | - ✨ Serverless ready: works even in browser. 24 | - 🛡 Security oriented: uses the most strict [Content Securiy Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) by default. 25 | - 📐 Standards based: uses WebAPI interfaces. 26 | - 🛳 Transport agnostic: no HTTP or platform coupling, ship requests via __everything__. 27 | 28 | --- 29 | 30 | ## Table of Contents 31 | 32 | * [Install](#install) 33 | * [Examples](#exmaples) 34 | * [Packages](#packages) 35 | * [Internal packages](#internal-packages) 36 | 37 | ## Install 38 | 39 | ```bash 40 | # Install plant web server 41 | npm i @plant/plant 42 | # Install node HTTP2 transport 43 | npm i @plant/http2 44 | ``` 45 | 46 | ## Examples 47 | 48 | ### Hello World 49 | 50 | Hello world with HTTP2 as transport. 51 | 52 | > ⚠️ Note that default CSP header value is `default-src localhost; form-action localhost`. 53 | > This will prevent web page from loading any external resource at all. 54 | > Set minimal required CSP on your own. Read about [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) on Mozilla Developer Network 55 | 56 | 57 | ```javascript 58 | // Build request handler 59 | const createServer = require('@plant/http2'); 60 | const Plant = require('@plant/plant'); 61 | 62 | const plant = new Plant(); 63 | plant.use(({res}) => { 64 | res.body = 'Hello, World!' 65 | }) 66 | 67 | createServer(plant) 68 | .listen(8080) 69 | ``` 70 | 71 | ### Router 72 | 73 | Plant's builtin router is extremely simple and works only with 74 | exact strings. But there is more powerful router package which brings named params and regular expressions into routing. 75 | 76 | ```javascript 77 | const Plant = require('@plant/plant'); 78 | const Router = require('@plant/router'); 79 | 80 | const plant = new Plant() 81 | const router = new Router() 82 | 83 | router.get('/user/:name', async function({res, route}) { 84 | res.body = `Hello, ${route.params.name}!` 85 | }) 86 | 87 | plant.use('/api/v1/*', router) 88 | ``` 89 | 90 | ### HTTP2 pushes 91 | 92 | Hello world with HTTP2 as transport. 93 | 94 | ```javascript 95 | // Build request handler 96 | const createServer = require('@plant/http2'); 97 | const Plant = require('@plant/plant'); 98 | 99 | const plant = new Plant(); 100 | 101 | plant.use('/script.js', ({res}) => { 102 | res.headers.set('content-type', 'application/javascript') 103 | res.body = 'console.log("Hello")' 104 | }) 105 | 106 | plant.use('/index.html', ({res, fetch}) => { 107 | // Push '/script.js' URL to pushed resources. 108 | // It will be requested before sending main response. 109 | res.push('/script.js') 110 | // ... or ... 111 | // Push complete response from subrequest 112 | res.push( 113 | await fetch('/script.js') 114 | ) 115 | 116 | res.body = '' 117 | }) 118 | 119 | createServer(plant) 120 | .listen(8080) 121 | ``` 122 | 123 | 124 | ## Packages 125 | 126 | ### [Router](packages/router) `@plant/router` 127 | 128 | [NPM](https://npmjs.com/package/@plant/router) · 129 | [Source](packages/router) · [Readme](packages/router/readme.md) 130 | 131 | Plant standalone router. 132 | 133 | ## HTTP(S) Packages 134 | 135 | ### [HTTP2](packages/http2) `@plant/http2` 136 | 137 | [NPM](https://npmjs.com/package/@plant/http2) · 138 | [Source](packages/http2) · [Readme](packages/http2/readme.md) 139 | 140 | Plant adapter for native node.js http2 module server. It creates server 141 | listener from Plant instance and `http2.createServer()` [options](https://nodejs.org/dist/latest-v11.x/docs/api/http2.html#http2_http2_createserver_options_onrequesthandler). It's 142 | usage is the same as https module. 143 | 144 | ### [HTTPS2](packages/https2) `@plant/https2` 145 | 146 | [NPM](https://npmjs.com/package/@plant/https2) · 147 | [Source](packages/https2) · [Readme](packages/https2/readme.md) 148 | 149 | Plant adapter for native node.js http2 module SSL server. It creates server 150 | listener from Plant instance and `http2.createSecureServer()` [options](https://nodejs.org/dist/latest-v11.x/docs/api/http2.html#http2_http2_createsecureserver_options_onrequesthandler). It's 151 | usage is the same as https module. 152 | 153 | ### [HTTP](packages/http) `@plant/http` 154 | 155 | [NPM](https://npmjs.com/package/@plant/http) · 156 | [Source](packages/http) · [Readme](packages/http/readme.md) 157 | 158 | Plant adapter for native node.js http module. It creates server listener from plant instance. 159 | 160 | ### [HTTPS](packages/https) `@plant/https` 161 | 162 | [NPM](https://npmjs.com/package/@plant/https) · 163 | [Source](packages/https) · [Readme](packages/https/readme.md) 164 | 165 | Plant adapter for native node.js https module. It creates server listener from plant instance and https options. 166 | 167 | ### [HTTP Adapter](packages/http-adapter) `@plant/http-adapter` 168 | 169 | [NPM](https://npmjs.com/package/@plant/http-adapter) · 170 | [Source](packages/http-adapter) · [Readme](packages/http-adapter/readme.md) 171 | 172 | This package is using to connect Plant and native Node's HTTP server. Modules http, https, http2, and https2 use it under the hood. 173 | 174 | ## Electron Packages 175 | 176 | ### [Electron](packages/electron-adapter) `@plant/electron` 177 | 178 | [NPM](https://npmjs.com/package/@plant/electron) · 179 | [Source](packages/electron) · [Readme](packages/electron/readme.md) 180 | 181 | This package is using to connect Plant and with current Electron instance protocols API. 182 | It's using `electron-adapter` under the hood. 183 | 184 | ### [Electron Adapter](packages/electron-adapter) `@plant/electron-adapter` 185 | 186 | [NPM](https://npmjs.com/package/@plant/electron-adapter) · 187 | [Source](packages/electron-adapter) · [Readme](packages/electron-adapter/readme.md) 188 | 189 | This package is using to connect Plant and with Electron protocols API. 190 | 191 | ## Utility Packages 192 | 193 | ### [Flow](packages/flow) `@plant/flow` 194 | 195 | [NPM](https://npmjs.com/package/@plant/flow) · 196 | [Source](packages/flow) · [Readme](packages/flow/readme.md) 197 | 198 | This is library for cascades. This is where contexts manage take place and requests pass from one handler to another. 199 | 200 | ### [Node Stream Utils](packages/node-stream-utils) `@plant/node-stream-utils` 201 | 202 | [NPM](https://npmjs.com/package/@plant/node-stream-utils) · 203 | [Source](packages/node-stream-utils) · [Readme](packages/node-stream-utils/readme.md) 204 | 205 | Node <-> WebAPI streams adapters. Useful for wrapping Node.js streams to work 206 | with Plant. 207 | 208 | ## Tests Packages 209 | 210 | ### [Test HTTP Suite](packages/test-http) `@plant/test-http` 211 | 212 | [NPM](https://npmjs.com/package/@plant/test-http) · 213 | [Source](packages/test-http) · [Readme](packages/test-http/readme.md) 214 | 215 | Tiny package with tools for HTTP testing. It simplify server creation and request sending and receiving. 216 | 217 | ## License 218 | 219 | MIT © [Rumkin](https://rumk.in) 220 | --------------------------------------------------------------------------------