├── .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 |
3 |
4 |
5 | # Plant
6 |
7 | [](https://npmjs.com/package/@plant/plant)
8 | [](https://npmjs.com/package/@plant/plant)
9 | 
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 |
--------------------------------------------------------------------------------