├── .prettierignore ├── .env.demo ├── docs ├── CNAME ├── img │ ├── code.png │ ├── logo.png │ ├── lanterns.jpg │ ├── portused.png │ ├── logo-auth0.png │ ├── chevron.svg │ ├── lock.svg │ ├── modern.svg │ ├── lego.svg │ ├── logo.svg │ ├── socketio.svg │ └── battery.svg ├── documentation │ ├── index.json │ ├── errors │ │ ├── index.html.pug │ │ └── README.md │ ├── index.html.pug │ ├── testing │ │ ├── index.html.pug │ │ └── README.md │ ├── context │ │ └── index.html.pug │ ├── reply │ │ └── index.html.pug │ ├── plugins │ │ └── index.html.pug │ ├── options │ │ └── index.html.pug │ ├── router │ │ └── index.html.pug │ ├── toc.pug │ └── documentation.pug ├── filters.js ├── tutorials │ ├── chat │ │ ├── img │ │ │ ├── mockup.png │ │ │ ├── screenshot.png │ │ │ ├── screenshot_final.png │ │ │ └── screenshot_prompt.png │ │ ├── index.html.pug │ │ └── index.json │ ├── spreadsheet │ │ ├── img │ │ │ ├── publish.png │ │ │ ├── list_cards.png │ │ │ ├── list_table.png │ │ │ ├── userlist.png │ │ │ ├── list_simple.png │ │ │ ├── spreadsheet.png │ │ │ ├── demographics_map.png │ │ │ └── demographics_chart.png │ │ ├── index.html.pug │ │ └── index.json │ ├── index.json │ ├── todo │ │ ├── img │ │ │ └── todo_screenshot.png │ │ ├── index.html.pug │ │ └── index.json │ ├── sessions-production │ │ ├── index.html.pug │ │ ├── index.json │ │ └── README.md │ ├── getting-started │ │ ├── index.html.pug │ │ ├── index.json │ │ └── README.md │ ├── toc.pug │ ├── tutorial.pug │ └── index.html.pug ├── _layout │ ├── mixins.pug │ ├── nav.pug │ └── index.pug ├── sponsor │ ├── index.html.pug │ └── README.md ├── README.md ├── files.js ├── assets │ ├── _nav.scss │ ├── _prism.scss │ ├── _article.scss │ └── _toc.scss ├── index.html.pug └── sitemap.xml ├── .npmignore ├── examples ├── reply │ ├── data.txt │ └── index.js ├── handlebars │ ├── views │ │ ├── partials │ │ │ └── nav.hbs │ │ ├── page.hbs │ │ └── index.hbs │ └── index.js ├── db │ ├── views │ │ ├── page.pug │ │ └── index.pug │ └── index.js ├── template │ ├── views │ │ ├── page.pug │ │ └── index.pug │ └── index.js ├── download-pdf │ ├── sample.pdf │ └── index.js ├── bug62 │ └── index.js ├── bug118 │ └── index.js ├── log │ └── index.js ├── bug132 │ └── index.js ├── supertest │ ├── index.js │ ├── package.json │ └── index.test.js ├── simple │ └── index.js ├── routes │ └── index.js ├── multiple │ └── index.js ├── env │ └── index.js ├── session │ └── index.js ├── benchmark │ └── index.js ├── bug43 │ └── index.js ├── bug44 │ ├── custom-error.js │ └── index.js ├── stream │ └── index.js ├── bug64 │ └── index.js ├── socket │ ├── index.js │ └── views │ │ └── index.html ├── websocket │ ├── index.js │ └── views │ │ └── index.html └── file-upload │ └── index.js ├── test ├── a.js ├── logo.ico ├── logo.png ├── views │ └── index.hbs ├── index.js ├── run.test.js ├── examples │ ├── test-0.test.js │ ├── test-1.test.js │ ├── test-2.test.js │ ├── test-4.test.js │ ├── test-3.test.js │ ├── test-5.test.js │ ├── test-6.test.js │ └── test-7.test.js ├── lint.test.js ├── port.js ├── examples.test.js └── run.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── workflows │ └── tests.yml └── ISSUE_TEMPLATE │ ├── Feature_request.md │ ├── Bug_report.md │ └── Ask_question.md ├── reply ├── end.js ├── file.js ├── json.js ├── send.js ├── type.js ├── cookie.js ├── header.js ├── jsonp.js ├── render.js ├── status.js ├── download.js ├── redirect.js ├── index.js ├── unit.test.js └── reply.js ├── router ├── parse.js ├── get.js ├── put.js ├── del.js ├── head.js ├── post.js ├── errors.js ├── sub.js ├── index.js ├── error.js └── generic.js ├── plugins ├── compress │ ├── index.js │ └── integration.test.js ├── favicon │ ├── integration.test.js │ └── index.js ├── static │ ├── index.js │ └── unit.test.js ├── security │ ├── unit.test.js │ └── index.js ├── session │ ├── unit.test.js │ └── index.js ├── log │ ├── index.js │ └── integration.test.js ├── final │ ├── errors.js │ ├── index.js │ └── final.test.js ├── express │ ├── integration.test.js │ └── index.js ├── socket │ └── index.js └── parser │ ├── index.js │ └── integration.test.js ├── src ├── index.test.js ├── modern │ ├── validate.js │ ├── index.js │ ├── errors.js │ └── modern.test.js ├── config │ ├── index.js │ ├── env.js │ ├── integration.test.js │ ├── schema.js │ ├── errors.js │ └── init.test.js └── join │ ├── unit.test.js │ ├── index.js │ └── integration.test.js ├── .eslintrc.json ├── error ├── index.js ├── README.md └── index.test.js ├── LICENSE ├── .gitignore ├── roadmap.md ├── Contributing.md ├── server.js ├── package.json ├── api.js ├── Gruntfile.js ├── changelog.md └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | ** -------------------------------------------------------------------------------- /.env.demo: -------------------------------------------------------------------------------- 1 | TEST44={"a"} -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | serverjs.io 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs 2 | examples 3 | test 4 | -------------------------------------------------------------------------------- /examples/reply/data.txt: -------------------------------------------------------------------------------- 1 | I am a file! 2 | -------------------------------------------------------------------------------- /test/a.js: -------------------------------------------------------------------------------- 1 | module.exports = ctx => '世界'; 2 | -------------------------------------------------------------------------------- /examples/handlebars/views/partials/nav.hbs: -------------------------------------------------------------------------------- 1 | This is a partial view! 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.paypal.me/franciscopresencia/19 2 | -------------------------------------------------------------------------------- /test/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/test/logo.ico -------------------------------------------------------------------------------- /test/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/test/logo.png -------------------------------------------------------------------------------- /docs/img/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/img/code.png -------------------------------------------------------------------------------- /docs/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/img/logo.png -------------------------------------------------------------------------------- /examples/db/views/page.pug: -------------------------------------------------------------------------------- 1 | p You are in page " 2 | strong= "/" + page 3 | | " 4 | -------------------------------------------------------------------------------- /examples/handlebars/views/page.hbs: -------------------------------------------------------------------------------- 1 |

You are in page {{page}}

2 | -------------------------------------------------------------------------------- /examples/template/views/page.pug: -------------------------------------------------------------------------------- 1 | p You are in page " 2 | strong= "/" + page 3 | | " 4 | -------------------------------------------------------------------------------- /docs/img/lanterns.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/img/lanterns.jpg -------------------------------------------------------------------------------- /docs/img/portused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/img/portused.png -------------------------------------------------------------------------------- /docs/img/logo-auth0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/img/logo-auth0.png -------------------------------------------------------------------------------- /docs/documentation/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | "options", 3 | "context", 4 | "router", 5 | "reply" 6 | ] 7 | -------------------------------------------------------------------------------- /docs/filters.js: -------------------------------------------------------------------------------- 1 | // Should be okay 2 | module.exports.noheader = block => block.replace(/.+?<\/h1>/g, ''); 3 | -------------------------------------------------------------------------------- /examples/download-pdf/sample.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/examples/download-pdf/sample.pdf -------------------------------------------------------------------------------- /reply/end.js: -------------------------------------------------------------------------------- 1 | const Reply = require('./reply'); 2 | 3 | module.exports = (...args) => new Reply('end', ...args); 4 | -------------------------------------------------------------------------------- /reply/file.js: -------------------------------------------------------------------------------- 1 | const Reply = require('./reply'); 2 | 3 | module.exports = (...args) => new Reply('file', ...args); 4 | -------------------------------------------------------------------------------- /reply/json.js: -------------------------------------------------------------------------------- 1 | const Reply = require('./reply'); 2 | 3 | module.exports = (...args) => new Reply('json', ...args); 4 | -------------------------------------------------------------------------------- /reply/send.js: -------------------------------------------------------------------------------- 1 | const Reply = require('./reply'); 2 | 3 | module.exports = (...args) => new Reply('send', ...args); 4 | -------------------------------------------------------------------------------- /reply/type.js: -------------------------------------------------------------------------------- 1 | const Reply = require('./reply'); 2 | 3 | module.exports = (...args) => new Reply('type', ...args); 4 | -------------------------------------------------------------------------------- /docs/tutorials/chat/img/mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/tutorials/chat/img/mockup.png -------------------------------------------------------------------------------- /reply/cookie.js: -------------------------------------------------------------------------------- 1 | const Reply = require('./reply'); 2 | 3 | module.exports = (...args) => new Reply('cookie', ...args); 4 | -------------------------------------------------------------------------------- /reply/header.js: -------------------------------------------------------------------------------- 1 | const Reply = require('./reply'); 2 | 3 | module.exports = (...args) => new Reply('header', ...args); 4 | -------------------------------------------------------------------------------- /reply/jsonp.js: -------------------------------------------------------------------------------- 1 | const Reply = require('./reply'); 2 | 3 | module.exports = (...args) => new Reply('jsonp', ...args); 4 | -------------------------------------------------------------------------------- /reply/render.js: -------------------------------------------------------------------------------- 1 | const Reply = require('./reply'); 2 | 3 | module.exports = (...args) => new Reply('render', ...args); 4 | -------------------------------------------------------------------------------- /reply/status.js: -------------------------------------------------------------------------------- 1 | const Reply = require('./reply'); 2 | 3 | module.exports = (...args) => new Reply('status', ...args); 4 | -------------------------------------------------------------------------------- /reply/download.js: -------------------------------------------------------------------------------- 1 | const Reply = require('./reply'); 2 | 3 | module.exports = (...args) => new Reply('download', ...args); 4 | -------------------------------------------------------------------------------- /reply/redirect.js: -------------------------------------------------------------------------------- 1 | const Reply = require('./reply'); 2 | 3 | module.exports = (...args) => new Reply('redirect', ...args); 4 | -------------------------------------------------------------------------------- /docs/tutorials/chat/img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/tutorials/chat/img/screenshot.png -------------------------------------------------------------------------------- /docs/tutorials/spreadsheet/img/publish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/tutorials/spreadsheet/img/publish.png -------------------------------------------------------------------------------- /docs/tutorials/chat/img/screenshot_final.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/tutorials/chat/img/screenshot_final.png -------------------------------------------------------------------------------- /docs/tutorials/chat/img/screenshot_prompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/tutorials/chat/img/screenshot_prompt.png -------------------------------------------------------------------------------- /docs/tutorials/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | "getting-started", 3 | "sessions-production", 4 | "spreadsheet", 5 | "todo", 6 | "chat" 7 | ] 8 | -------------------------------------------------------------------------------- /docs/tutorials/spreadsheet/img/list_cards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/tutorials/spreadsheet/img/list_cards.png -------------------------------------------------------------------------------- /docs/tutorials/spreadsheet/img/list_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/tutorials/spreadsheet/img/list_table.png -------------------------------------------------------------------------------- /docs/tutorials/spreadsheet/img/userlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/tutorials/spreadsheet/img/userlist.png -------------------------------------------------------------------------------- /docs/tutorials/todo/img/todo_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/tutorials/todo/img/todo_screenshot.png -------------------------------------------------------------------------------- /examples/bug62/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../'); 2 | 3 | // Should log the { status: 404 } error in the console 4 | server(); 5 | -------------------------------------------------------------------------------- /examples/db/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../server'); 2 | const { render } = server.reply; 3 | 4 | server(ctx => render('index')); 5 | -------------------------------------------------------------------------------- /docs/tutorials/spreadsheet/img/list_simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/tutorials/spreadsheet/img/list_simple.png -------------------------------------------------------------------------------- /docs/tutorials/spreadsheet/img/spreadsheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/tutorials/spreadsheet/img/spreadsheet.png -------------------------------------------------------------------------------- /examples/bug118/index.js: -------------------------------------------------------------------------------- 1 | const server = require("../../"); 2 | const { get, error } = server.router; 3 | 4 | server(get("/", ctx => "Hello world")); 5 | -------------------------------------------------------------------------------- /docs/tutorials/spreadsheet/img/demographics_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/tutorials/spreadsheet/img/demographics_map.png -------------------------------------------------------------------------------- /docs/tutorials/spreadsheet/img/demographics_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscop/server/HEAD/docs/tutorials/spreadsheet/img/demographics_chart.png -------------------------------------------------------------------------------- /examples/db/views/index.pug: -------------------------------------------------------------------------------- 1 | p Hello world 2 | p Visit some pages: 3 | 4 | p 5 | a(href="/hithere") /hithere 6 | 7 | p 8 | a(href="/dynamic") /dynamic 9 | -------------------------------------------------------------------------------- /examples/template/views/index.pug: -------------------------------------------------------------------------------- 1 | p Hello world 2 | p Visit some pages: 3 | 4 | p 5 | a(href="/hithere") /hithere 6 | 7 | p 8 | a(href="/dynamic") /dynamic 9 | -------------------------------------------------------------------------------- /examples/log/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../'); 2 | const { json } = server.reply; 3 | 4 | server(ctx => { 5 | ctx.log.info('Hi there'); 6 | }, ctx => 'Hello!'); 7 | -------------------------------------------------------------------------------- /examples/bug132/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../'); 2 | 3 | const { get } = server.router; 4 | const { status } = server.reply; 5 | 6 | server([get(ctx => status(404))]); 7 | -------------------------------------------------------------------------------- /examples/supertest/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../server'); 2 | const { get } = server.router; 3 | 4 | module.exports = server(get('/user', () => ({ name: 'john' }))); 5 | -------------------------------------------------------------------------------- /docs/img/chevron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /examples/handlebars/views/index.hbs: -------------------------------------------------------------------------------- 1 |

Hello world

2 |

Visit some pages:

3 |

/hithere

4 |

/dynamic

5 | {{> partials/nav}} 6 | -------------------------------------------------------------------------------- /router/parse.js: -------------------------------------------------------------------------------- 1 | // Parse the request parameters 2 | module.exports = middle => { 3 | const path = typeof middle[0] === 'string' ? middle.shift() : '*'; 4 | return { path, middle }; 5 | }; 6 | -------------------------------------------------------------------------------- /examples/simple/index.js: -------------------------------------------------------------------------------- 1 | // The simplest of the examples. Navigate to the folder, run 'node index.js' 2 | // and open the browser on 'localhost:3000' 3 | require('../../server')(ctx => 'Hello 世界'); 4 | -------------------------------------------------------------------------------- /examples/routes/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../server'); 2 | const { get, post } = server.router; 3 | 4 | server([ 5 | get('/', ctx => 'Hello 世界'), 6 | get('/page', ctx => 'Hello page') 7 | ]); 8 | -------------------------------------------------------------------------------- /examples/multiple/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../server'); 2 | const { get, post } = server.router; 3 | 4 | server(3000, get('/', ctx => 'Hello 3000')); 5 | server(4000, get('/', ctx => 'Hello 4000')); 6 | -------------------------------------------------------------------------------- /router/get.js: -------------------------------------------------------------------------------- 1 | // Import the generic REST handler 2 | const generic = require('./generic'); 3 | 4 | // Defined the actual method 5 | module.exports = (...middle) => { 6 | return generic('GET', ...middle); 7 | }; 8 | -------------------------------------------------------------------------------- /router/put.js: -------------------------------------------------------------------------------- 1 | // Import the generic REST handler 2 | const generic = require('./generic'); 3 | 4 | // Defined the actual method 5 | module.exports = (...middle) => { 6 | return generic('PUT', ...middle); 7 | }; 8 | -------------------------------------------------------------------------------- /test/views/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 |

Hello world

9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/env/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../server'); 2 | 3 | server({ 4 | port: 3000, 5 | random_variable: 'a' 6 | }).then(ctx => { 7 | console.log(ctx.options.port, ctx.options.random_variable); 8 | }); 9 | -------------------------------------------------------------------------------- /router/del.js: -------------------------------------------------------------------------------- 1 | // Import the generic REST handler 2 | const generic = require('./generic'); 3 | 4 | // Defined the actual method 5 | module.exports = (...middle) => { 6 | return generic('DELETE', ...middle); 7 | }; 8 | -------------------------------------------------------------------------------- /router/head.js: -------------------------------------------------------------------------------- 1 | // Import the generic REST handler 2 | const generic = require('./generic'); 3 | 4 | // Defined the actual method 5 | module.exports = (...middle) => { 6 | return generic('HEAD', ...middle); 7 | }; 8 | -------------------------------------------------------------------------------- /router/post.js: -------------------------------------------------------------------------------- 1 | // Import the generic REST handler 2 | const generic = require('./generic'); 3 | 4 | // Defined the actual method 5 | module.exports = (...middle) => { 6 | return generic('POST', ...middle); 7 | }; 8 | -------------------------------------------------------------------------------- /docs/documentation/errors/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ../documentation 2 | 3 | block title 4 | +title('Errors') 5 | 6 | block description 7 | +description('Create and handle errors.') 8 | 9 | block article 10 | include:marked ./README.md 11 | -------------------------------------------------------------------------------- /docs/documentation/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ./documentation 2 | 3 | block title 4 | +title('Documentation') 5 | 6 | block description 7 | +description('Full documentation of server.js API and methodologies.') 8 | 9 | block article 10 | include:marked ./README.md 11 | -------------------------------------------------------------------------------- /docs/documentation/testing/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ../documentation 2 | 3 | block title 4 | +title('Testing documentation') 5 | 6 | block description 7 | +description('Testing your application properly.') 8 | 9 | block article 10 | include:marked ./README.md 11 | -------------------------------------------------------------------------------- /examples/template/index.js: -------------------------------------------------------------------------------- 1 | const server = require("../../server"); 2 | const { get, post } = server.router; 3 | const { render } = server.reply; 4 | 5 | server( 6 | get("/", (ctx) => render("index")), 7 | get("/:id", (ctx) => render("page", { page: ctx.params.id })) 8 | ); 9 | -------------------------------------------------------------------------------- /examples/supertest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@server/supertest", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "jest" 7 | }, 8 | "devDependencies": { 9 | "jest": "^27.0.3", 10 | "supertest": "^6.1.3" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /router/errors.js: -------------------------------------------------------------------------------- 1 | // YOU MIGHT BE LOOKING FOR 'error.js' (without the "s") 2 | 3 | const error = require('../error')('/server/test/'); 4 | 5 | error.router = 'This is a demo error'; 6 | error.simplerouter = ({ text }) => `Simple message: ${text}`; 7 | 8 | module.exports = error; 9 | -------------------------------------------------------------------------------- /examples/handlebars/index.js: -------------------------------------------------------------------------------- 1 | const server = require("../../server"); 2 | const { get, post } = server.router; 3 | const { render } = server.reply; 4 | 5 | server( 6 | get("/", (ctx) => render("index.hbs")), 7 | get("/:id", (ctx) => render("page.hbs", { page: ctx.params.id })) 8 | ); 9 | -------------------------------------------------------------------------------- /docs/tutorials/sessions-production/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ../tutorial 2 | 3 | block title 4 | +title('Session in production') 5 | 6 | block description 7 | +description('Learn how to set-up the session for production.') 8 | 9 | block article 10 | include:marked ./README.md 11 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // The test suite and the different needed parts 2 | module.exports = { 3 | 4 | // Generate a random port that is not already in use 5 | port: require('./port'), 6 | 7 | // Handle a function that expects to be thrown 8 | // throws: require('./throws') 9 | }; 10 | -------------------------------------------------------------------------------- /docs/documentation/context/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ../documentation 2 | 3 | block title 4 | +title('Context documentation') 5 | 6 | block description 7 | +description('All of the properties of the argument for the middleware.') 8 | 9 | block article 10 | include:marked ./README.md 11 | -------------------------------------------------------------------------------- /docs/documentation/reply/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ../documentation 2 | 3 | block title 4 | +title('Reply documentation') 5 | 6 | block description 7 | +description('How to handle different types of responses for any situation.') 8 | 9 | block article 10 | include:marked ./README.md 11 | -------------------------------------------------------------------------------- /docs/tutorials/getting-started/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ../tutorial 2 | 3 | block title 4 | +title('Getting started') 5 | 6 | block description 7 | +description('Install and setup a Node.js project from scratch to get started.') 8 | 9 | block article 10 | include:marked ./README.md 11 | -------------------------------------------------------------------------------- /examples/session/index.js: -------------------------------------------------------------------------------- 1 | // Simple visit counter for the main page 2 | const server = require("../../server"); 3 | const counter = (ctx) => { 4 | const session = ctx.session; 5 | session.views = (session.views || 0) + 1; 6 | return `Session: ${session.views}`; 7 | }; 8 | server(counter); 9 | -------------------------------------------------------------------------------- /docs/tutorials/chat/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ../tutorial 2 | 3 | block title 4 | +title('Chat real-time') 5 | 6 | block description 7 | +description('Create a real-time chat with websockets to talk with anyone who visits your site.') 8 | 9 | block article 10 | include:marked ./README.md 11 | -------------------------------------------------------------------------------- /docs/documentation/plugins/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ../documentation 2 | 3 | block title 4 | +title('Plugins documentation') 5 | 6 | block description 7 | +description('Information about how to create a plugin and why those are useful.') 8 | 9 | block article 10 | include:marked ./README.md 11 | -------------------------------------------------------------------------------- /test/run.test.js: -------------------------------------------------------------------------------- 1 | let run; 2 | 3 | describe('run() main function', () => { 4 | it('is a function', async () => { 5 | run = require('./run'); 6 | expect(run instanceof Function).toBe(true); 7 | }); 8 | 9 | it('can be called empty', async () => { 10 | return run(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /docs/documentation/options/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ../documentation 2 | 3 | block title 4 | +title('Options documentation') 5 | 6 | block description 7 | +description('The different options and how to use them to make Node.js development easier.') 8 | 9 | block article 10 | include:marked ./README.md 11 | -------------------------------------------------------------------------------- /examples/benchmark/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../server'); 2 | const { get } = server.router; 3 | 4 | server({ port: 3001, middle: false }, get('/', ctx => 'Hello 世界')); 5 | 6 | const express = require('express'); 7 | const app = express(); 8 | app.get('/', ctx => 'Hello 世界'); 9 | app.listen(2000); 10 | -------------------------------------------------------------------------------- /docs/tutorials/chat/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Chat in real-time", 3 | "description": "Create a real-time chat with websockets to talk with anyone who visits your site", 4 | "sections": [ 5 | "Websockets in server", 6 | "Choose a username", 7 | "Display messages", 8 | "Send messages" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /docs/documentation/router/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ../documentation 2 | 3 | block title 4 | +title('Router documentation') 5 | 6 | block description 7 | +description('Handle different types of requests, including making a REST API, websockets and subdomain management.') 8 | 9 | block article 10 | include:marked ./README.md 11 | -------------------------------------------------------------------------------- /docs/tutorials/getting-started/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Getting started", 3 | "description": "Install Node.js and related files to get started developing with server.js.", 4 | "sections": [ 5 | "Install Node", 6 | "Create your project", 7 | "Initialize Git and npm", 8 | "Make awesome things" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /docs/tutorials/todo/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ../tutorial 2 | 3 | block title 4 | +title('TO-DO tutorial') 5 | 6 | block description 7 | +description('Simple TO-DO website using jQuery for the AJAX. Define an API to create, read, update and delete items from a MongoDB database.') 8 | 9 | block article 10 | include:marked ./README.md 11 | -------------------------------------------------------------------------------- /examples/bug43/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../'); 2 | const { status } = server.reply; 3 | const { get } = server.router; 4 | 5 | server( 6 | // This should get invoked for every request 7 | // An alternative syntax would be to support Regular Expressions. 8 | get('*', (ctx) => { 9 | return status(200); 10 | }) 11 | ); 12 | -------------------------------------------------------------------------------- /docs/tutorials/sessions-production/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sessions in production", 3 | "description": "Learn how to set-up the session for production using Redis or other available datastores so there's persistence even after a server restart.", 4 | "sections": [ 5 | "Secret", 6 | "Storage", 7 | "Alternatives" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /docs/tutorials/spreadsheet/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ../tutorial 2 | 3 | block title 4 | +title('Spreadsheet tutorial') 5 | 6 | block description 7 | +description('Learn how to use a Google Spreadsheet as a database for web applications. Perfect for the confluence of business and web.') 8 | 9 | block article 10 | include:marked ./README.md 11 | -------------------------------------------------------------------------------- /docs/tutorials/todo/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Todo list", 3 | "description": "Simple TO-DO website using jQuery for the AJAX. Define an API to create, read, update and delete items from a MongoDB database.", 4 | "sections": [ 5 | "Install Node", 6 | "Create your project", 7 | "Initialize Git and npm", 8 | "Make awesome things" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /docs/tutorials/spreadsheet/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Spreadsheets database", 3 | "description": "Simple TO-DO website using jQuery for the AJAX. Define an API to create, read, update and delete items from a MongoDB database.", 4 | "sections": [ 5 | "Install Node", 6 | "Create your project", 7 | "Initialize Git and npm", 8 | "Make awesome things" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /docs/_layout/mixins.pug: -------------------------------------------------------------------------------- 1 | mixin title(text) 2 | title= text ? text + ' - server.js' : 'Server.js' 3 | meta(property='og:title' content=text ? text + ' - server.js' : 'Server.js') 4 | 5 | mixin description(text) 6 | meta(name='description' content=text || 'Flexible and powerful server for Node.js') 7 | meta(property='og:description' content=text || 'Flexible and powerful server for Node.js') 8 | -------------------------------------------------------------------------------- /docs/sponsor/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ../_layout/index 2 | 3 | block title 4 | +title('Sponsor us <3') 5 | 6 | block description 7 | +description('Sponsor server.js to support its mission. Reach many developers with your logo and get a custom workshop for your company!') 8 | 9 | block content 10 | .hero 11 | .content 12 | h1 13 | strong Sponsor 14 | 15 | article 16 | include:marked:noheader ./README.md 17 | -------------------------------------------------------------------------------- /examples/bug44/custom-error.js: -------------------------------------------------------------------------------- 1 | const CustomError = function (message, status = 500, options = {}) { 2 | this.message = message; 3 | this.status = status; 4 | for (let key in options) { 5 | this[key] = options[key]; 6 | } 7 | this.code = 'custom'; // This has special meaning but it is not documented yet; just don't set it to 'server' 8 | }; 9 | CustomError.prototype = new Error; 10 | 11 | module.exports = CustomError; 12 | -------------------------------------------------------------------------------- /plugins/compress/index.js: -------------------------------------------------------------------------------- 1 | const modern = require('../../src/modern'); 2 | const compress = require('compression'); 3 | 4 | module.exports = { 5 | name: 'compress', 6 | options: { 7 | __root: 'compress', 8 | compress: { 9 | default: {}, 10 | type: Object 11 | } 12 | }, 13 | 14 | // The whole plugin won't be loaded if the option is false 15 | before: ctx => modern(compress(ctx.options.compress))(ctx) 16 | }; 17 | -------------------------------------------------------------------------------- /docs/img/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | // Test runner: 2 | const run = require('server/test/run'); 3 | 4 | describe('Default modules', () => { 5 | it('can log the context', async () => { 6 | const res = await run(ctx => { 7 | 8 | try { 9 | require('util').inspect(ctx); 10 | } catch (err) { 11 | return err.message; 12 | } 13 | return 'Good!'; 14 | }).get('/'); 15 | expect(res.body).toBe('Good!'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/stream/index.js: -------------------------------------------------------------------------------- 1 | const server = require("../../server"); 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | 5 | const img = path.resolve("../../test/logo.png"); 6 | const stream = (read, write) => 7 | new Promise((resolve, reject) => { 8 | read 9 | .pipe(write) 10 | .on("error", reject) 11 | .on("end", resolve); 12 | }); 13 | 14 | server(ctx => { 15 | return stream(fs.createReadStream(img), ctx.res); 16 | }); 17 | -------------------------------------------------------------------------------- /plugins/favicon/integration.test.js: -------------------------------------------------------------------------------- 1 | const run = require('server/test/run'); 2 | 3 | const favicon = 'test/logo.png'; 4 | 5 | describe('Default modules', () => { 6 | it('favicon', async () => { 7 | const res = await run({ favicon }).get('/favicon.ico'); 8 | expect(res.headers['content-type']).toBe('image/x-icon'); 9 | }); 10 | 11 | // TODO: test for non-existing 12 | 13 | // TODO: test different locations 14 | 15 | // TODO: test for env 16 | }); 17 | -------------------------------------------------------------------------------- /plugins/static/index.js: -------------------------------------------------------------------------------- 1 | const modern = require('../../src/modern'); 2 | 3 | module.exports = { 4 | name: 'static', 5 | options: { 6 | __root: 'public', 7 | public: { 8 | type: String, 9 | inherit: 'public', 10 | env: false 11 | } 12 | }, 13 | init: ctx => { 14 | if (!ctx.options.static.public) return; 15 | module.exports.before = [ 16 | modern(ctx.express.static(ctx.options.static.public)) 17 | ]; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /router/sub.js: -------------------------------------------------------------------------------- 1 | const join = require('../src/join'); 2 | 3 | module.exports = (path, ...middle) => async ctx => { 4 | const full = ctx.req.subdomains.reverse().join('.'); 5 | if ( 6 | (typeof path === 'string' && path === full) || 7 | (path instanceof RegExp && path.test(full)) 8 | ) { 9 | await join(middle, ctx => { 10 | ctx.req.solved = true; 11 | if (!ctx.res.headersSent) { 12 | ctx.res.end(); 13 | } 14 | })(ctx); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /docs/tutorials/toc.pug: -------------------------------------------------------------------------------- 1 | section.toc 2 | h2 3 | a(href="/tutorials/#top") Tutorials 4 | ul 5 | each tutorial, frag in tutorials 6 | li 7 | if (tutorial.sections.length) 8 | label.more 9 | a(href="/tutorials/" + frag + "/")= tutorial.title 10 | if (tutorial.sections.length) 11 | ul 12 | each name in tutorial.sections 13 | li 14 | a(href="/tutorials/" + frag + "/#" + slug(name))= name 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 8 4 | }, 5 | "plugins": ["jasmine"], 6 | "env": { 7 | "browser": true, 8 | "commonjs": true, 9 | "es6": true, 10 | "jasmine": true, 11 | "node": true 12 | }, 13 | "extends": "eslint:recommended", 14 | "rules": { 15 | "indent": ["error",2], 16 | "linebreak-style": ["error", "unix"], 17 | "quotes": ["error", "single"], 18 | "semi": ["error", "always"], 19 | "no-console": "off" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /reply/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cookie: require('./cookie' ), 3 | download: require('./download'), 4 | end: require('./end' ), 5 | file: require('./file' ), 6 | header: require('./header' ), 7 | json: require('./json' ), 8 | jsonp: require('./jsonp' ), 9 | redirect: require('./redirect'), 10 | render: require('./render' ), 11 | send: require('./send' ), 12 | status: require('./status' ), 13 | type: require('./type' ) 14 | }; 15 | -------------------------------------------------------------------------------- /plugins/favicon/index.js: -------------------------------------------------------------------------------- 1 | const modern = require('../../src/modern'); 2 | const favicon = require('serve-favicon'); 3 | 4 | module.exports = { 5 | name: 'favicon', 6 | options: { 7 | __root: 'location', 8 | location: { 9 | type: String, 10 | file: true, 11 | env: 'FAVICON' 12 | } 13 | }, 14 | 15 | before: [ 16 | ctx => { 17 | if (!ctx.options.favicon.location) return false; 18 | return modern(favicon(ctx.options.favicon.location))(ctx); 19 | } 20 | ] 21 | }; 22 | -------------------------------------------------------------------------------- /examples/bug64/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../'); 2 | const { get, error } = server.router; 3 | 4 | const mid2 = ctx => { 5 | const err = new Error('No username detected'); 6 | err.code = 'user.noname'; 7 | throw err; 8 | }; 9 | 10 | server( 11 | mid2, 12 | get('/', ctx=> json('hello world')), 13 | error('user', ctx => { 14 | console.log(ctx.error); 15 | return 'Matched! :)'; 16 | }), 17 | error(ctx => { 18 | console.log(ctx.error); 19 | return 'Not matched! :('; 20 | }) 21 | ); 22 | -------------------------------------------------------------------------------- /docs/_layout/nav.pug: -------------------------------------------------------------------------------- 1 | nav 2 | a.brand(href='/') 3 | img.logo(src='/img/logo.svg' alt='logo') 4 | span.text server.js 5 | 6 | input#bmenu.show(type='checkbox') 7 | label.burger.pseudo.button.switch(for='bmenu') menu 8 | .menu 9 | a.pseudo.button(href='https://medium.com/server-for-node-js' target='_blank') Blog 10 | a.pseudo.button(href='https://github.com/franciscop/server' target='_blank') Github 11 | a.pseudo.button(href='/tutorials') Tutorials 12 | a.button(href='/documentation') Documentation 13 | -------------------------------------------------------------------------------- /docs/tutorials/tutorial.pug: -------------------------------------------------------------------------------- 1 | extends ../_layout/index 2 | 3 | block extra 4 | div.width-1100 5 | 6 | block content 7 | article.tutorial 8 | div.flex 9 | //- Need this so the parent of .toc has a width and we position it respect. 10 | include ./toc.pug 11 | div.main 12 | block article 13 | 14 | h2#keep-reading Keep reading 15 | 16 | p Subscribe to our Mailchimp list to receive more tutorials when released: 17 | 18 | a.button(href="http://eepurl.com/cGRggH") Get Great Tutorials 19 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # WEBSITE 2 | 3 | This folder only contains the website: [**server** website](https://serverjs.io/) 4 | 5 | You might be looking for the folder "Documentation": [**documentation folder**](documentation) 6 | 7 | Which can also be accessed through the website: [**documentation in the website**](https://serverjs.io/documentation) 8 | 9 | > Note: I've contacted Github about how ridiculous it is to have to name the *web* as *docs* and suggested that they allow the name... *web*. They answered quickly and said it was in their tasklog, so kudos :+1:. 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | 5 | ### Expected outcome 6 | 7 | 8 | 9 | ### Actual outcome 10 | 11 | 12 | 13 | ### Live Demo 14 | 15 | 16 | 17 | ### System Information 18 | 19 | **OS:** MacOS High Sierra, Windows 10, etc. 20 | 21 | **Node Version:** v8.9.1 22 | 23 | **Server.js Version:** 1.0.14 24 | -------------------------------------------------------------------------------- /examples/socket/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../server'); 2 | 3 | const { get, socket } = server.router; 4 | const { render } = server.reply; 5 | 6 | module.exports = server([ 7 | get('/', ctx => render('index.html')), 8 | 9 | socket('connect', ctx => { 10 | console.log('Connected'); 11 | }), 12 | 13 | socket('disconnect', ctx => { 14 | console.log('Disconnected'); 15 | }), 16 | 17 | socket('hello', ctx => { 18 | console.log('Ping;', ctx.data); 19 | ctx.io.emit('there'); 20 | }), 21 | 22 | get('/favicon.ico', () => 404) 23 | ]); 24 | -------------------------------------------------------------------------------- /plugins/security/unit.test.js: -------------------------------------------------------------------------------- 1 | const run = require('server/test/run'); 2 | const { get, post } = require('server/router'); 3 | 4 | describe('static plugin', () => { 5 | it('csurf', async () => { 6 | return await run({ public: 'test' }, [ 7 | get('/', ctx => ctx.res.locals.csrf), 8 | post('/', () => '世界') 9 | ]).alive(async api => { 10 | const csrf = (await api.get('/')).body; 11 | expect(csrf).toBeDefined(); 12 | const res = await api.post('/', { body: { _csrf: csrf }}); 13 | expect(res.statusCode).toBe(200); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /docs/documentation/toc.pug: -------------------------------------------------------------------------------- 1 | section.toc 2 | h2 3 | a(href="/documentation/") Documentation 4 | form.searchform 5 | input.search(placeholder="search") 6 | div.searchbox 7 | ul 8 | each section, frag in documentation 9 | li 10 | if (section.sections.length) 11 | label.more 12 | a(href=section.url || "/documentation/" + frag + "/")= section.title 13 | if (section.sections.length) 14 | ul 15 | each name in section.sections 16 | li 17 | a(href=(section.url || "/documentation/" + frag + '/') + "#" + slug(name))= name 18 | -------------------------------------------------------------------------------- /examples/bug44/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../'); 2 | const { get, error } = server.router; 3 | const { status } = server.reply; 4 | const CustomError = require('./custom-error'); 5 | 6 | const homePageRouter = get('/', 7 | // Validation and checks 8 | ctx => { 9 | throw new CustomError('Some custom error', 400); 10 | throw new CustomError('Some other custom error', 402); 11 | }, 12 | 13 | // Normal middleware here 14 | ctx => 'Hello world', 15 | 16 | // Error handling 17 | error(ctx => status(ctx.error.status).json({ error: ctx.error.message })) 18 | ); 19 | 20 | server(homePageRouter); 21 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | platform: [ubuntu-latest, macos-latest, windows-latest] 12 | node-version: [16.x, 18.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: Installing 21 | run: npm install 22 | - name: Testing 23 | run: npm test 24 | env: 25 | CI: true 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🙏 Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /plugins/compress/integration.test.js: -------------------------------------------------------------------------------- 1 | const run = require('server/test/run'); 2 | 3 | describe('compress', () => { 4 | it('works with the defaults', async () => { 5 | const res = await run(() => 'Hello world').get('/'); 6 | expect(res.body).toBe('Hello world'); 7 | }); 8 | 9 | it('works with an empty option object', async () => { 10 | const res = await run({ compress: {} }, () => 'Hello world').get('/'); 11 | expect(res.body).toBe('Hello world'); 12 | }); 13 | 14 | it('works without compress', async () => { 15 | const res = await run({ compress: false }, () => 'Hello world').get('/'); 16 | expect(res.body).toBe('Hello world'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /docs/img/modern.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /router/index.js: -------------------------------------------------------------------------------- 1 | // Define the router methods that are available to use as middleware 2 | // Each of these is available through: 3 | // const { get } = require('server').router; 4 | // const { get } = require('server/router'); 5 | // const get = require('server/router/get'); 6 | 7 | // Perform the routing required 8 | module.exports = { 9 | // REST 10 | get: require('./get'), 11 | head: require('./head'), 12 | post: require('./post'), 13 | put: require('./put'), 14 | del: require('./del'), 15 | 16 | // Special cases 17 | sub: require('./sub'), 18 | error: require('./error'), 19 | join: require('../src/join'), 20 | socket: require('../plugins/socket').router 21 | }; 22 | -------------------------------------------------------------------------------- /test/examples/test-0.test.js: -------------------------------------------------------------------------------- 1 | // Test automatically retrieved. Do not edit manually 2 | const { render, json } = require('server/reply'); 3 | const { get, post } = require('server/router'); 4 | const { modern } = require('server').utils; 5 | const run = require('server/test/run'); 6 | const fs = require('mz/fs'); 7 | const path = require('path'); 8 | 9 | describe('Automatic test from content 0', () => { 10 | it('works', async () => { 11 | // START 12 | const mid = ctx => { 13 | expect(ctx.options.port).toBe(3012); 14 | }; 15 | 16 | /* test */ 17 | const res = await run({ port: 3012 }, mid, () => 200).get('/'); 18 | expect(res.status).toBe(200); 19 | // END 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/examples/test-1.test.js: -------------------------------------------------------------------------------- 1 | // Test automatically retrieved. Do not edit manually 2 | const { render, json } = require('server/reply'); 3 | const { get, post } = require('server/router'); 4 | const { modern } = require('server').utils; 5 | const run = require('server/test/run'); 6 | const fs = require('mz/fs'); 7 | const path = require('path'); 8 | 9 | describe('Automatic test from content 1', () => { 10 | it('works', async () => { 11 | // START 12 | const mid = ctx => { 13 | expect(ctx.options.port).toBe(7693); 14 | }; 15 | 16 | /* test */ 17 | const res = await run({ port: 7693 }, mid, () => 200).get('/'); 18 | expect(res.status).toBe(200); 19 | // END 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/modern/validate.js: -------------------------------------------------------------------------------- 1 | const ModernError = require('./errors'); 2 | 3 | exports.middleware = middle => { 4 | if (!middle) { 5 | throw new ModernError('missingmiddleware'); 6 | } 7 | if (!(middle instanceof Function)) { 8 | throw new ModernError('invalidmiddleware', { type: typeof middle }); 9 | } 10 | if (middle.length === 4) { 11 | throw new ModernError('errormiddleware'); 12 | } 13 | }; 14 | 15 | exports.context = ctx => { 16 | if (!ctx) { 17 | throw new ModernError('missingcontext'); 18 | } 19 | if (!ctx.req) { 20 | throw new ModernError('malformedcontext', { item: 'res' }); 21 | } 22 | if (!ctx.res) { 23 | throw new ModernError('malformedcontext', { item: 'res' }); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /test/examples/test-2.test.js: -------------------------------------------------------------------------------- 1 | // Test automatically retrieved. Do not edit manually 2 | const { render, json } = require('server/reply'); 3 | const { get, post } = require('server/router'); 4 | const { modern } = require('server').utils; 5 | const run = require('server/test/run'); 6 | const fs = require('mz/fs'); 7 | const path = require('path'); 8 | 9 | describe('Automatic test from content 2', () => { 10 | it('works', async () => { 11 | // START 12 | const options = { 13 | port: 5001 14 | }; 15 | 16 | /* test */ 17 | const same = ctx => ({ port: ctx.options.port }); 18 | const res = await run(options, same).get('/'); 19 | expect(res.body.port).toBe(5001); 20 | // END 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | const parse = require('./parse'); 2 | const schema = require('./schema'); 3 | const env = require('./env'); 4 | 5 | // Accept the user options (first argument) and then a list with all the plugins 6 | // This will allow us to use the plugin's schemas as well 7 | module.exports = async (user = {}, plugins = []) => { 8 | 9 | // First and most important is the core and the user-defined options 10 | const options = await parse(schema, user, env); 11 | 12 | // Then load plugin options namespaced with the name in parallel 13 | await Promise.all(plugins.map(async ({ name, options: def = {}} = {}) => { 14 | options[name] = await parse(def, user[name], env, options); 15 | })); 16 | 17 | return options; 18 | }; 19 | -------------------------------------------------------------------------------- /src/modern/index.js: -------------------------------------------------------------------------------- 1 | // Modern - Create a modern middleware from the old-style one 2 | 3 | // Cleanly validate data 4 | const validate = require('./validate'); 5 | 6 | // Pass it an old middleware and return a new one 'ctx => promise' 7 | module.exports = middle => { 8 | 9 | // Validate it early so no requests need to be made 10 | validate.middleware(middle); 11 | 12 | // Create and return the modern middleware function 13 | return ctx => new Promise((resolve, reject) => { 14 | validate.context(ctx); 15 | 16 | // It can handle both success or errors. Pass the right ctx 17 | const next = err => err ? reject(err) : resolve(); 18 | 19 | // Call the old middleware 20 | middle(ctx.req, ctx.res, next); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /docs/files.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = walk = function(dir, done) { 5 | done = done || (() => {}); 6 | let results = []; 7 | let list = fs.readdirSync(dir); 8 | 9 | var pending = list.length; 10 | if (!pending) return done(null, results); 11 | list.forEach(function(file) { 12 | file = path.resolve(dir, file); 13 | let stat = fs.statSync(file); 14 | if (stat && stat.isDirectory()) { 15 | walk(file, function(err, res) { 16 | results = results.concat(res); 17 | if (!--pending) done(null, results); 18 | }); 19 | } else { 20 | results.push(file); 21 | if (!--pending) done(null, results); 22 | } 23 | }); 24 | return results; 25 | }; 26 | -------------------------------------------------------------------------------- /test/lint.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { exec } = require('mz/child_process'); 3 | 4 | const command = path.resolve(process.cwd() + '/node_modules/.bin/eslint'); 5 | 6 | const lint = src => new Promise((resolve, reject) => { 7 | const place = path.resolve(__dirname + '/../' + src); 8 | // const src = path.resolve(__dirname + '/../server.js'); 9 | exec(`${command} ${place}`, (err, stdout, stderr) => { 10 | if (stdout.length) return reject(stdout); 11 | resolve(); 12 | }); 13 | }); 14 | 15 | describe('linter', () => { 16 | it('lints the src', async () => { 17 | return await lint('src'); 18 | }, 100000); 19 | 20 | it('lints the plugins', async () => { 21 | return await lint('plugins'); 22 | }, 100000); 23 | }); 24 | -------------------------------------------------------------------------------- /examples/supertest/index.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | // The promise exported from your index 4 | const serverPromise = require('./index.js'); 5 | 6 | // This will be populated after the server has launched. You can call it 7 | // anything you want; server, runtime, ctx, instance, etc. are all valid names 8 | let server; 9 | 10 | describe('user', () => { 11 | beforeAll(async () => { 12 | server = await serverPromise; 13 | }); 14 | afterAll(async () => { 15 | await server.close(); 16 | }); 17 | it('tests the user endpoint', async () => { 18 | await request(server.app) 19 | .get('/user') 20 | .expect('Content-Type', /json/) 21 | .expect('Content-Length', '15') 22 | .expect(200); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/examples/test-4.test.js: -------------------------------------------------------------------------------- 1 | // Test automatically retrieved. Do not edit manually 2 | const { render, json } = require('server/reply'); 3 | const { get, post } = require('server/router'); 4 | const { modern } = require('server').utils; 5 | const run = require('server/test/run'); 6 | const fs = require('mz/fs'); 7 | const path = require('path'); 8 | 9 | describe('Automatic test from content 4', () => { 10 | it('works', async () => { 11 | // START 12 | const options = { 13 | public: './' 14 | }; 15 | 16 | /* test */ 17 | const same = ctx => ({ public: ctx.options.public }); 18 | const res = await run(options, same).get('/'); 19 | expect(res.body.public).toBe(process.cwd() + path.sep); 20 | // END 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/examples/test-3.test.js: -------------------------------------------------------------------------------- 1 | // Test automatically retrieved. Do not edit manually 2 | const { render, json } = require('server/reply'); 3 | const { get, post } = require('server/router'); 4 | const { modern } = require('server').utils; 5 | const run = require('server/test/run'); 6 | const fs = require('mz/fs'); 7 | const path = require('path'); 8 | 9 | describe('Automatic test from content 3', () => { 10 | it('works', async () => { 11 | // START 12 | const options = { 13 | public: 'public' 14 | }; 15 | 16 | /* test */ 17 | const same = ctx => ({ public: ctx.options.public }); 18 | const res = await run(options, same).get('/'); 19 | expect(res.body.public).toBe(path.join(process.cwd() + '/public')); 20 | // END 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/examples/test-5.test.js: -------------------------------------------------------------------------------- 1 | // Test automatically retrieved. Do not edit manually 2 | const { render, json } = require('server/reply'); 3 | const { get, post } = require('server/router'); 4 | const { modern } = require('server').utils; 5 | const run = require('server/test/run'); 6 | const fs = require('mz/fs'); 7 | const path = require('path'); 8 | 9 | describe('Automatic test from content 5', () => { 10 | it('works', async () => { 11 | // START 12 | const options = { 13 | views: 'views' 14 | }; 15 | 16 | /* test */ 17 | const same = ctx => ({ views: ctx.options.views }); 18 | const res = await run(options, same).get('/'); 19 | expect(res.body.views).toBe(path.join(process.cwd(), 'views') + path.sep); 20 | // END 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /examples/websocket/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../server'); 2 | const { get, socket } = server.router; 3 | const { render } = server.reply; 4 | 5 | server({ 6 | socket: { 7 | path: '/custompath' 8 | } 9 | }, 10 | get('/', ctx => { 11 | ctx.session.counter = ctx.session.counter || 0; 12 | return render('index.html'); 13 | }), 14 | socket('connect', ctx => { 15 | 16 | // Emit an event every second with +1 on the session 17 | setInterval(() => { 18 | 19 | // Increment the counter 20 | ctx.session.counter++; 21 | 22 | // For socket.io you need to manually save it 23 | ctx.session.save(); 24 | 25 | // Send the value to the currently connected socket 26 | ctx.socket.emit('message', ctx.session.counter); 27 | }, 1000); 28 | }) 29 | ); 30 | -------------------------------------------------------------------------------- /examples/websocket/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

Hello world!

11 | 12 | 13 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/config/env.js: -------------------------------------------------------------------------------- 1 | // Load them from the environment file if any 2 | require('dotenv').config({ silent: true }); 3 | 4 | // Check if a variable is numeric even if string 5 | const is = { 6 | numeric: num => !isNaN(num), 7 | boolean: bool => /^(true|false)$/i.test(bool), 8 | json: str => /^[{[]/.test(str) && /[}\]]$/.test(str) 9 | }; 10 | 11 | const type = str => { 12 | if (!str) return; 13 | if (typeof str !== 'string') return str; 14 | if (is.numeric(str)) return +str; 15 | if (is.boolean(str)) return /true/i.test(str); 16 | try { 17 | if (is.json(str)) return JSON.parse(str); 18 | } catch (err) { 19 | return str; 20 | } 21 | return str; 22 | }; 23 | 24 | const env = {}; 25 | for (let key in process.env) { 26 | env[key] = type(process.env[key]); 27 | } 28 | 29 | module.exports = env; 30 | -------------------------------------------------------------------------------- /docs/documentation/documentation.pug: -------------------------------------------------------------------------------- 1 | extends ../_layout/index 2 | 3 | block extra 4 | div.width-1100 5 | 6 | block content 7 | article.documentation 8 | div.flex 9 | //- Need this so the parent of .toc has a width and we position it respect. 10 | include ./toc.pug 11 | div.main 12 | block article 13 | 14 | - var topics = require('./docs/documentation/index.json'); 15 | 16 | h2#keep-reading Keep reading 17 | 18 | p List of all the topics: 19 | 20 | div.pages 21 | a.button(href="/documentation/") Introduction 22 | a.button(href="/documentation/options/") Options 23 | a.button(href="/documentation/context/") Context 24 | a.button(href="/documentation/router/") Router 25 | a.button(href="/documentation/reply/") Reply 26 | -------------------------------------------------------------------------------- /test/examples/test-6.test.js: -------------------------------------------------------------------------------- 1 | // Test automatically retrieved. Do not edit manually 2 | const { render, json } = require('server/reply'); 3 | const { get, post } = require('server/router'); 4 | const { modern } = require('server').utils; 5 | const run = require('server/test/run'); 6 | const fs = require('mz/fs'); 7 | const path = require('path'); 8 | 9 | describe('Automatic test from content 6', () => { 10 | it('works', async () => { 11 | // START 12 | const options = { 13 | views: './templates' 14 | }; 15 | 16 | /* test */ 17 | options.views = './test/views'; 18 | const same = ctx => ({ views: ctx.options.views }); 19 | const res = await run(options, same).get('/'); 20 | expect(res.body.views).toBe(process.cwd() + path.sep + 'test/views' + path.sep); 21 | // END 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /examples/download-pdf/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const util = require("util"); 3 | const server = require("../../server"); 4 | const { header } = server.reply; 5 | 6 | // Read it and store it in a variable, emulating a pdf generated on-the-fly 7 | const read = util.promisify(fs.readFile); 8 | const generatePdf = () => read("./sample.pdf", "utf-8"); 9 | 10 | // Trigger the download with server.js 11 | const downloadPdf = (data, name) => { 12 | return header({ "Content-Disposition": `attachment;filename="${name}"` }) 13 | .type("application/pdf") 14 | .send(new Buffer(data)); 15 | }; 16 | 17 | server(async () => { 18 | // It can be created manually, file read, or other ways 19 | const pdf = await generatePdf(); 20 | 21 | // Prompt the browser to download it with a new name 22 | return downloadPdf(pdf, "test.pdf"); 23 | }); 24 | -------------------------------------------------------------------------------- /test/port.js: -------------------------------------------------------------------------------- 1 | // Port - generate a random valid port number that has not been used yet 2 | 3 | // Get an unused port in the user range (2000 - 10000) 4 | const ports = []; 5 | const limit = 1000; 6 | 7 | const random = () => 1024 + parseInt(Math.random() * 48151); 8 | 9 | // Keep a count of how many times we tried to find a port to avoid infinite loop 10 | const randPort = (i = 0) => { 11 | 12 | // Too many ports tried and none was available 13 | if (i >= limit) { 14 | throw new Error('Tried to find a port but none seems available'); 15 | } 16 | 17 | const port = random(); 18 | 19 | // If "i" is already taken try again 20 | if (port in ports) { 21 | return randPort(i + 1); 22 | } 23 | 24 | // Add it to the list of ports already being used and return it 25 | ports.push(port); 26 | return port; 27 | } 28 | 29 | module.exports = randPort; 30 | -------------------------------------------------------------------------------- /examples/file-upload/index.js: -------------------------------------------------------------------------------- 1 | const server = require("../../server"); 2 | const { get, post } = server.router; 3 | 4 | const form = ` 5 | 6 | 7 | File Upload Demo 8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 | 16 | `; 17 | 18 | server( 19 | { security: { csrf: false } }, 20 | get("/", () => form), 21 | post("/", (ctx) => { 22 | // Here is your file, "picture" as in name="picture" in the form: 23 | console.log(ctx.files.picture); 24 | console.log("Path:", ctx.files.picture.path); 25 | 26 | return ctx.files.picture; 27 | }), 28 | get("/favicon.ico", () => 404) 29 | ); 30 | -------------------------------------------------------------------------------- /docs/img/lego.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /error/index.js: -------------------------------------------------------------------------------- 1 | const buildError = (message, opts) => { 2 | const error = new Error(message); 3 | for (const key in opts) { 4 | error[key] = opts[key] instanceof Function ? opts[key](opts) : opts[key]; 5 | } 6 | return error; 7 | }; 8 | 9 | const singleSlash = str => '/' + str.split('/').filter(one => one).join('/'); 10 | 11 | const ErrorFactory = function (namespace = '', defaults = {}) { 12 | defaults.namespace = defaults.namespace || namespace; 13 | 14 | return function ErrorInstance (code = '', options = {}) { 15 | options = Object.assign({}, ErrorFactory.options, defaults, options); 16 | options.code = singleSlash(options.namespace + '/' + code); 17 | options.id = options.code.toLowerCase().replace(/[^\w]+/g, '-').replace(/^-/, ''); 18 | options.message = ErrorInstance[code]; 19 | return buildError(options.message, options); 20 | }; 21 | }; 22 | 23 | ErrorFactory.options = { status: 500 }; 24 | 25 | module.exports = ErrorFactory; 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /router/error.js: -------------------------------------------------------------------------------- 1 | const join = require('../src/join'); 2 | const parse = require('./parse'); 3 | const { match } = require('path-to-regexp'); 4 | 5 | const decode = decodeURIComponent; 6 | 7 | module.exports = (...all) => { 8 | // Extracted or otherwise it'd shift once per call; also more performant 9 | const { path, middle } = parse(all); 10 | 11 | // Convert to the proper path, since the new ones use `(.*)` instead of `*` 12 | const parsePath = match(path.replace(/\*/g, '(.*)'), { decode: decode }); 13 | 14 | const generic = () => {}; 15 | generic.error = async ctx => { 16 | 17 | // Only do this if the correct path 18 | ctx.error.code = ctx.error.code || ''; 19 | ctx.error.params = parsePath(ctx.error.code).params; 20 | 21 | // Add an extra-allowing initial matching 22 | if (!ctx.error.params && ctx.error.code.slice(0, path.length) !== path) return; 23 | 24 | const ret = await middle[0](ctx); 25 | delete ctx.error; 26 | return ret; 27 | }; 28 | return generic; 29 | }; 30 | -------------------------------------------------------------------------------- /test/examples/test-7.test.js: -------------------------------------------------------------------------------- 1 | // Test automatically retrieved. Do not edit manually 2 | const { render, json } = require('server/reply'); 3 | const { get, post } = require('server/router'); 4 | const { modern } = require('server').utils; 5 | const run = require('server/test/run'); 6 | const fs = require('mz/fs'); 7 | const path = require('path'); 8 | 9 | describe('Automatic test from content 7', () => { 10 | it('works', async () => { 11 | // START 12 | // Simple visit counter for the main page 13 | const counter = get('/', ctx => { 14 | ctx.session.views = (ctx.session.views || 0) + 1; 15 | return { views: ctx.session.views }; 16 | }); 17 | 18 | /* test */ 19 | await run(counter).alive(async api => { 20 | let res = await api.get('/'); 21 | expect(res.body.views).toBe(1); 22 | res = await api.get('/'); 23 | expect(res.body.views).toBe(2); 24 | res = await api.get('/'); 25 | expect(res.body.views).toBe(3); 26 | }); 27 | // END 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /plugins/session/unit.test.js: -------------------------------------------------------------------------------- 1 | const run = require('server/test/run'); 2 | const { get } = require('server/router'); 3 | const send = require('server/reply/send'); 4 | 5 | describe('static plugin', () => { 6 | it('can handle sessions', async () => { 7 | return run({ public: 'test' }, [ 8 | get('/a', ctx => { 9 | ctx.session.page = 'pageA'; 10 | return send(''); 11 | }), 12 | get('/b', ctx => send(ctx.session.page)) 13 | ]).alive(async api => { 14 | expect((await api.get('/a')).body).toEqual(''); 15 | expect((await api.get('/b')).body).toEqual('pageA'); 16 | }); 17 | }); 18 | 19 | it('persists the session', async () => { 20 | const mid = ctx => { 21 | ctx.session.counter = (ctx.session.counter || 0) + 1; 22 | return 'n' + ctx.session.counter; 23 | }; 24 | return run(mid).alive(async api => { 25 | for (let i = 0; i < 3; i++) { 26 | const res = await api.get('/'); 27 | expect(res.body).toBe('n' + (i + 1)); 28 | } 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /docs/_layout/index.pug: -------------------------------------------------------------------------------- 1 | include ./mixins.pug 2 | 3 | doctype html 4 | html.initial 5 | head 6 | block title 7 | +title() 8 | meta(charset='utf-8') 9 | meta(name='viewport' content='width=device-width, initial-scale=1') 10 | meta(name='keywords' content='server, javascript, js, node.js, library, html, html5, express') 11 | block description 12 | +description('Flexible and powerful server for Node.js') 13 | link(rel='shortcut icon' type='image/png' href='/img/logo.png') 14 | meta(property='og:url' content='http://serverjs.io/') 15 | meta(property='og:image' content='https://serverjs.io/img/code.png') 16 | link(href='/assets/style.min.css' rel='stylesheet') 17 | 18 | body#top 19 | block extra 20 | 21 | include nav 22 | 23 | block content 24 | 25 | script(src="https://unpkg.com/paperdocs@1.0.9/paperdocs.min.js") 26 | script(src="https://unpkg.com/smoothscroll-polyfill@0.4.0/dist/smoothscroll.js") 27 | //- include ../assets/umbrella.js 28 | script 29 | include ../assets/javascript.js 30 | 31 | include ./mixins 32 | -------------------------------------------------------------------------------- /src/join/unit.test.js: -------------------------------------------------------------------------------- 1 | // const join = require('./index.js'); 2 | 3 | describe('Performance', () => { 4 | it('dummy', () => {}); 5 | 6 | // it('takes time to join promises', () => { 7 | // const cb = ctx => ctx.count++; 8 | // 9 | // const times = [10, 100, 1000, 10000, 100000]; 10 | // const promises = times.map(k => ctx => { 11 | // const proms = []; 12 | // for (let i = 0; i < k; i++) { 13 | // proms.push(cb); 14 | // } 15 | // console.time('chained-' + k); 16 | // return join(proms)({ count: 0 }).then(ctx => { 17 | // console.timeEnd('chained-' + k); 18 | // }); 19 | // }); 20 | // 21 | // return join(promises)({}); 22 | // }); 23 | }); 24 | 25 | 26 | // Similar: 27 | // Native: 28 | // chained-10: 0.272ms 29 | // chained-100: 1.052ms 30 | // chained-1000: 13.040ms 31 | // chained-10000: 81.560ms 32 | // chained-100000: 882.968ms 33 | 34 | // Bluebird (slower): 35 | // chained-10: 1.711ms 36 | // chained-100: 7.160ms 37 | // chained-1000: 13.631ms 38 | // chained-10000: 78.505ms 39 | // chained-100000: 749.576ms 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 serverjs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /plugins/log/index.js: -------------------------------------------------------------------------------- 1 | const log = require('npmlog'); 2 | 3 | const valid = [ 4 | 'debug', 5 | 'info', 6 | 'notice', 7 | 'warning', 8 | 'error', 9 | 'critical', 10 | 'alert', 11 | 'emergency' 12 | ]; 13 | 14 | // Log plugin 15 | const plugin = { 16 | name: 'log', 17 | options: { 18 | __root: 'level', 19 | level: { 20 | default: 'info', 21 | type: String, 22 | enum: valid 23 | }, 24 | report: { 25 | default: process.stdout 26 | } 27 | }, 28 | init: ctx => { 29 | valid.forEach((level, n) => { 30 | log.addLevel(level, n); 31 | }); 32 | log.level = 'info'; 33 | if (ctx.options.log.level) { 34 | log.level = ctx.options.log.level; 35 | } 36 | ctx.log = {}; 37 | valid.forEach(type => { 38 | ctx.log[type] = content => { 39 | if ( 40 | ctx.options.log.report && 41 | typeof ctx.options.log.report === 'function' 42 | ) { 43 | ctx.options.log.report(content, type); 44 | } 45 | log.log(type, '', content); 46 | }; 47 | }); 48 | } 49 | }; 50 | 51 | module.exports = plugin; 52 | -------------------------------------------------------------------------------- /examples/reply/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../../server'); 2 | const { get } = server.router; 3 | const { status, file, render, download, jsonp } = server.reply; 4 | 5 | const links = [ 6 | 'text', 'html', 'json_arr', 'json_obj', 'jsonp_arr?callback=foo', 7 | 'jsonp_obj?callback=foo', 'file', 'render', 'status', 'download', 'mixed' 8 | ]; 9 | 10 | server([ 11 | get('/', ctx => `Try:
${links.map(link => `/${link}`).join('
')}`), 12 | 13 | // Base: 14 | get('/text', ctx => 'Hello 世界'), 15 | get('/html', ctx => '

Hello 世界

'), 16 | get('/json_arr', ctx => ['a', 'b', 'c']), 17 | get('/json_obj', ctx => ({ a: 'b', c: 'd' })), 18 | get('/jsonp_arr', ctx => jsonp(['a', 'b', 'c'])), 19 | get('/jsonp_obj', ctx => jsonp(({ a: 'b', c: 'd' }))), 20 | 21 | // Needed: 22 | get('/file', ctx => file('data.txt')), 23 | get('/render', ctx => render('index.pug')), 24 | get('/renderhtml', ctx => render('index.html')), 25 | get('/download', ctx => download('data.txt', 'my file.txt')), 26 | 27 | // Concatenable: 28 | get('/status', ctx => status(200)), 29 | get('/mixed', ctx => status(201).file('data.txt')), 30 | ]); 31 | -------------------------------------------------------------------------------- /docs/assets/_nav.scss: -------------------------------------------------------------------------------- 1 | 2 | nav .menu > * { 3 | margin: 0; 4 | } 5 | 6 | @media all and (min-width: #{$picnic-breakpoint}) { 7 | nav .menu { 8 | padding-right: .6em; 9 | } 10 | } 11 | 12 | 13 | nav { 14 | padding-left: calc(50% - 450px); 15 | padding-right: calc(50% - 450px); 16 | margin: 0 auto; 17 | } 18 | 19 | :not(.extra.docs) ~ nav.transparent { 20 | background: none; 21 | box-shadow: none; 22 | 23 | a, a.pseudo, .burger { 24 | color: #fff; 25 | } 26 | } 27 | 28 | @each $size in (500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500) { 29 | .width-#{$size} ~ nav { 30 | padding-left: calc(50% - #{$size * 0.5}px); 31 | padding-right: calc(50% - #{$size * 0.5}px); 32 | position: absolute; 33 | } 34 | 35 | .width-#{$size} ~ article { 36 | max-width: #{$size}px; 37 | } 38 | } 39 | 40 | nav .brand { 41 | padding: 0 .5em; 42 | } 43 | 44 | nav .burger { 45 | margin-right: .5em; 46 | } 47 | 48 | nav .brand .logo { 49 | margin-right: .75em; 50 | } 51 | 52 | nav .pseudo { 53 | margin-right: 5px; 54 | } 55 | 56 | @media all and (max-width: $picnic-breakpoint) { 57 | nav.transparent .menu a.pseudo { 58 | color: #333; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /router/generic.js: -------------------------------------------------------------------------------- 1 | const join = require('../src/join'); 2 | const parse = require('./parse'); 3 | const { match } = require('path-to-regexp'); 4 | 5 | const decode = decodeURIComponent; 6 | 7 | // Generic request handler 8 | module.exports = (method, ...all) => { 9 | // Extracted or otherwise it'd shift once per call; also more performant 10 | const { path, middle } = parse(all); 11 | 12 | // Convert to the proper path, since the new ones use `(.*)` instead of `*` 13 | const parsePath = match(path.replace(/\*/g, '(.*)'), { decode: decode }); 14 | 15 | return async ctx => { 16 | // A route should be solved only once per request 17 | if (ctx.req.solved) return; 18 | 19 | // Only for the correct method 20 | if (method !== ctx.req.method) return; 21 | 22 | // Only do this if the correct path 23 | ctx.req.params = parsePath(ctx.req.path).params; 24 | if (!ctx.req.params) return; 25 | ctx.params = ctx.req.params; 26 | 27 | // Perform this promise chain 28 | await join(middle, ctx => { 29 | // Only solve it if all the previous middleware succeeded 30 | ctx.req.solved = true; 31 | if (!ctx.res.headersSent) { 32 | ctx.res.end(); 33 | } 34 | })(ctx); 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/config/integration.test.js: -------------------------------------------------------------------------------- 1 | // Test runner: 2 | const run = require('server/test/run'); 3 | const path = require('path'); 4 | 5 | const test = 'test'; 6 | 7 | describe('Basic router types', () => { 8 | // TODO: fix this 9 | it('has independent options', async () => { 10 | const res = await Promise.all([ 11 | run({ public: 'right' }, ctx => new Promise(resolve => { 12 | setTimeout(() => { 13 | ctx.res.send(ctx.options.public); 14 | resolve(); 15 | }, 1000); 16 | })).get('/'), 17 | run({ public: 'wrong' }, ctx => ctx.options.public).get('/') 18 | ]); 19 | 20 | expect(res[0].body).toMatch(/right/); 21 | expect(res[1].body).toMatch(/wrong/); 22 | }); 23 | 24 | it('accepts several definitions of public correctly', async () => { 25 | const full = path.join(process.cwd(), 'test'); 26 | const publish = ctx => ctx.options.public; 27 | 28 | expect((await run({ 29 | public: test 30 | }, publish).get('/')).body).toBe(full); 31 | 32 | expect((await run({ 33 | public: './' + test 34 | }, publish).get('/')).body).toBe(full); 35 | 36 | expect((await run({ 37 | public: __dirname + '/../../' + test 38 | }, publish).get('/')).body).toBe(full); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /plugins/final/errors.js: -------------------------------------------------------------------------------- 1 | const error = require('../../error')('/plugin/final/'); 2 | 3 | error.noreturn = ({ method, url }) => ` 4 | Your middleware did not return anything for this request: 5 | 6 | ${method} ${url} 7 | 8 | This normally happens when no route was matched or if the router did not reply with anything. Make sure to return something, even if it's a catch-all error. 9 | 10 | Documentation for reply: https://serverjs.io/documentation/reply/ 11 | Relevant issue: https://github.com/franciscop/server/issues/118 12 | `; 13 | 14 | error.unhandled = ` 15 | Some middleware threw an error that was not handled properly. This can happen when you do this: 16 | 17 | ~~~ 18 | // BAD: 19 | server(ctx => { throw new Error('I am an error!'); }); 20 | ~~~ 21 | 22 | To catch and handle these types of errors, add a route to the end of your middlewares to handle errors like this: 23 | 24 | ~~~ 25 | // GOOD: 26 | const { error } = server.router; 27 | const { status } = server.reply; 28 | 29 | server( 30 | ctx => { throw new Error('I am an error!'); }, 31 | // ... 32 | error(ctx => status(500).send(ctx.error.message)) 33 | ); 34 | ~~~ 35 | 36 | Please feel free to open an issue in Github asking for more info: 37 | https://github.com/franciscop/server 38 | `; 39 | 40 | module.exports = error; 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Temporary folder 9 | /temp 10 | 11 | # Mac temporal file 12 | .DS_Store 13 | 14 | # SASS Cache 15 | .sass-cache 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # Yarn Integrity file 64 | .yarn-integrity 65 | 66 | # dotenv environment variables file 67 | .env 68 | 69 | # next.js build output 70 | .next 71 | 72 | # This is a library so don't include it 73 | package-lock.json 74 | -------------------------------------------------------------------------------- /examples/socket/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 |

Ping Pong

13 | 14 | 15 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /plugins/session/index.js: -------------------------------------------------------------------------------- 1 | const modern = require('../../src/modern'); 2 | const session = require('express-session'); 3 | const RedisStore = require('connect-redis'); 4 | const Redis = require('ioredis'); 5 | 6 | let sessionMiddleware; 7 | module.exports = { 8 | name: 'session', 9 | options: { 10 | __root: 'secret', 11 | resave: { 12 | default: false 13 | }, 14 | saveUninitialized: { 15 | default: true 16 | }, 17 | cookie: { 18 | default: {} 19 | }, 20 | secret: { 21 | type: String, 22 | inherit: 'secret', 23 | env: 'SESSION_SECRET' 24 | }, 25 | store: { 26 | env: false 27 | }, 28 | redis: { 29 | type: String, 30 | inherit: true, 31 | env: 'REDIS_URL' 32 | } 33 | }, 34 | init: ctx => { 35 | if (!ctx.options.session.store && ctx.options.session.redis) { 36 | const redisClient = new Redis(ctx.options.session.redis); 37 | ctx.options.session.store = new RedisStore({ client: redisClient }); 38 | } 39 | sessionMiddleware = session(ctx.options.session); 40 | }, 41 | before: ctx => modern(sessionMiddleware)(ctx), 42 | launch: ctx => { 43 | // Return early if the Socket plugin is not enabled 44 | if (!ctx.io || !ctx.io.use) return; 45 | ctx.io.use(function (socket, next) { 46 | sessionMiddleware(socket.request, socket.request.res || {}, next); 47 | }); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /docs/sponsor/README.md: -------------------------------------------------------------------------------- 1 | # Sponsor 2 | 3 | This project is maintained by Francisco Presencia and is part of Francisco IO LTD (UK). It is a lot of work and I'd love if you or your company could help me keep building it and in the process I'll help you with Node.js. 4 | 5 | **All sponsors will receive an ebook and/or a book** for free when released. Besides this, there are some sponsors tiers: 6 | 7 | | sponsorship | perk | credit (homepage + github) | 8 | |--------------|--------------------------------|------------------------------| 9 | | $1,000+ | email support | logo (normal) + link + ♥ | 10 | | $2,000+ | live coding help of 10h | logo (normal) + link + ♥ | 11 | | $10,000+ | in-person workshop of 20h | logo (large) + link + ♥ | 12 | 13 | Get in touch 14 | 15 | 16 | 17 | ## Notes and conditions 18 | 19 | Donations are 0-99.99$ and sponsorships are 100$+. 20 | 21 | All of the perks have a valid period of 1 year, later on they'd have to be renewed. The book/ebook has no duration and has to happen once. 22 | 23 | I reserve the right to reject anything if I don't find it suitable, including but not limited to malign requests. 24 | 25 | Everything in this page is negotiable and will be specified when getting in touch: 26 | 27 | **Thank you a lot for helping me improve server.js!** 28 | 29 | Get in touch Paypal me 30 | -------------------------------------------------------------------------------- /error/README.md: -------------------------------------------------------------------------------- 1 | # Experimental syntax proposal 2 | 3 | ```js 4 | // Using a factory 5 | const ErrorFactory = require('server/error'); 6 | const ServerError = ErrorFactory('/server/', {}); 7 | 8 | const ErrorFactory = require('server/error'); 9 | class ServerError extends ErrorFactory { 10 | namespace: '/server', 11 | url: ({ id }) => ..., 12 | status: 500 13 | } 14 | 15 | // Using the plain import method 16 | const ServerError = require('server/error')('/server/'); 17 | const ServerError = require('server/error')('/server/', { 18 | namespace: '/server/', 19 | url: ({ slug }) => `https://serverjs.io/documentation/errors/#${slug}`, 20 | status: 500, 21 | }); 22 | const ServerError = require('server/error')({ 23 | namespace: '/server/' 24 | }); 25 | 26 | const SassError = require('server/error')({ 27 | namespace: '/plugin/sass', 28 | url: ({ slug }) => `https://serverjs.io/documentation/errors/#${slug}`, 29 | }); 30 | 31 | const ServerError = require('server/error'); 32 | 33 | const SassError = ServerError(null, { 34 | namespace: '/plugin/sass', 35 | status: 500 36 | }); 37 | 38 | const SassError = ServerError.defaults({ 39 | namespace: '/plugin/sass/', 40 | status: 500 41 | }); 42 | 43 | SassError.exists = ({ file }) => ` 44 | The file "${file}" already exists. Please rename it or remove it so @sass/server 45 | can work properly. <3 46 | `; 47 | 48 | throw new SassError('exists'); 49 | throw new SassError('exists', { status: 500 }); 50 | throw new SassError('exists', { file: FILENAME }); 51 | ``` 52 | -------------------------------------------------------------------------------- /plugins/final/index.js: -------------------------------------------------------------------------------- 1 | // This file makes sure to clean up things in case there was something missing 2 | // There are two reasons normally for this to happen: no reply was set or an 3 | // unhandled error was thrown 4 | const FinalError = require('./errors'); 5 | 6 | // Make sure that a (404) reply is sent if there was no user reply 7 | const handler = async ctx => { 8 | if (!ctx.res.headersSent) { 9 | // Send the user-set status 10 | ctx.res.status(ctx.res.explicitStatus ? ctx.res.statusCode : 404).send(); 11 | 12 | // Show it only if there was no status set in a return 13 | if (!ctx.res.explicitStatus) { 14 | ctx.log.error( 15 | new FinalError('noreturn', { url: ctx.url, method: ctx.method }) 16 | ); 17 | } 18 | } 19 | }; 20 | 21 | // Make sure there is a (500) reply if there was an unhandled error thrown 22 | handler.error = ctx => { 23 | const error = ctx.error; 24 | ctx.log.warning(FinalError('unhandled')); 25 | ctx.log.error(error); 26 | if (!ctx.res.headersSent) { 27 | let status = error.status || error.code || 500; 28 | if (typeof status !== 'number') status = 500; 29 | 30 | // Display the error message if this error is marked as public 31 | if (error.public) { 32 | return ctx.res.status(status).send(error.message); 33 | } 34 | 35 | // Otherwise just display the default error for that code 36 | ctx.res.sendStatus(status); 37 | } 38 | }; 39 | 40 | module.exports = { 41 | name: 'final', 42 | after: handler 43 | }; 44 | // module.exports = handler; 45 | -------------------------------------------------------------------------------- /plugins/static/unit.test.js: -------------------------------------------------------------------------------- 1 | const run = require('server/test/run'); 2 | const stat = require('./'); 3 | 4 | const storeLog = out => ({ 5 | report: log => { 6 | out.log = log.toString(); 7 | } 8 | }); 9 | 10 | describe('static plugin', () => { 11 | it('exists', () => { 12 | expect(stat).toBeDefined(); 13 | expect(stat.name).toBe('static'); 14 | expect(stat.options).toBeDefined(); 15 | }); 16 | 17 | it('static', async () => { 18 | const res = await run({ public: 'test' }).get('/logo.png'); 19 | expect(res.statusCode).toBe(200); 20 | expect(res.headers['content-type']).toBe('image/png'); 21 | }); 22 | 23 | it('non-existing static', async () => { 24 | let out = {}; 25 | const log = storeLog(out); 26 | const res = await run({ public: 'xxxx', log }).get('/non-existing.png'); 27 | 28 | expect(res.statusCode).toBe(404); 29 | expect(out.log).toMatch(/did not return anything/); 30 | }); 31 | 32 | it('does not serve if set to false', async () => { 33 | let out = {}; 34 | const log = storeLog(out); 35 | const res = await run({ public: false, log }).get('/logo.png'); 36 | 37 | expect(res.statusCode).toBe(404); 38 | expect(out.log).toMatch(/did not return anything/); 39 | }); 40 | 41 | it('does not serve if set to empty string', async () => { 42 | let out = {}; 43 | const log = storeLog(out); 44 | const res = await run({ public: '', log }).get('/logo.png'); 45 | 46 | expect(res.statusCode).toBe(404); 47 | expect(out.log).toMatch(/did not return anything/); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | Wish list and features. Totally tentative, but nothing set in stone. 4 | 5 | 6 | ## Version 1.2 7 | 8 | Passport integration. Performance. 9 | 10 | 11 | ## Version 1.1 12 | 13 | Making Plugin API. Integrate websockets: 14 | 15 | ```js 16 | let server = require('server'); 17 | let { socket } = server.router; 18 | 19 | server({}, [ 20 | 21 | // These come from user-events 22 | socket('join', ctx => ctx.io.emit('join', ctx.data)), 23 | socket('message', ctx => ctx.io.emit('message', ctx.data)), 24 | 25 | // These are from the native events 26 | socket('connect', ctx => { /* ... */ }), 27 | socket('disconnect', ctx => { /* ... */ }) 28 | ]); 29 | ``` 30 | 31 | This will require some serious handling, but in exchange will make websockets easily accessible to everyone. 32 | 33 | 34 | 35 | ## Version 1.0 36 | 37 | > This is being rushed because NPM asked me to publish 1.x as there were already 0.x version from other person, so version 1.0 will be published with few alphas/betas 38 | 39 | Retrieve the old functionality of Express to make it easy to launch a server in Node.js 40 | 41 | Todo: 42 | 43 | - Testing testing and more testing 44 | - Good documentation and [tutorials in Libre University](https://en.libre.university/subject/4kitSFzUe) 45 | 46 | Done: 47 | 48 | - Include all of the libraries 49 | - Created the base 50 | - Implemented some of the libraries 51 | - Use it in real-world projects 52 | - Make sure that the express-session is secure with the secret (session usage depend on whether the token is provided or not) 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Ask_question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🙋‍ Ask a Question 3 | about: Ask any kind of question 4 | 5 | --- 6 | 7 | **What are you trying to do?** 8 | Please describe first **what you are trying to do** before your actual problem. Some times there is a conceptually simpler way of doing things, but we get stuck into the details of our way. This is known as [the XY Problem](https://meta.stackexchange.com/q/66377/179259). 9 | 10 | **Have you tried this first?** 11 | Since server is maintained mainly only by [Francisco Presencia](https://francisco.io/), it is recommended that you try to fix the problem on your own before asking a question. But if you have tried and couldn't fix it, feel free to ask! Make sure to do this first though: 12 | 13 | - [ ] Try to find an answer by reading [the documentation](https://serverjs.io/documentation/) and [tutorials](https://serverjs.io/tutorials/). 14 | - [ ] Try to find an answer by searching [previous Github issues](https://github.com/franciscop/server/issues/). 15 | - [ ] Try to find an answer by searching the Web. 16 | - [ ] Try to find an answer by inspection or experimentation. 17 | - [ ] Try to find an answer by asking a skilled friend. 18 | - [ ] Try to find an answer by reading the source code. 19 | 20 | **Ask your question** 21 | A clear and concise description of what you want to achieve and the issues you are facing. 22 | 23 | **Environment (optional):** 24 | - OS: 25 | - Node.js: 26 | - Server.js: 27 | 28 | **Additional context (optional)** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /src/config/schema.js: -------------------------------------------------------------------------------- 1 | const buffer = require('crypto').randomBytes(60); 2 | const token = buffer.toString('base64').replace(/\//g,'_').replace(/\+/g,'-'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | __root: 'port', 7 | port: { 8 | default: 3000, 9 | type: Number 10 | }, 11 | public: { 12 | default: 'public', 13 | type: [String, Boolean], 14 | file: true, 15 | clean: (value, option) => { 16 | if (/^win/.test(process.platform) && value === 'C:\\Users\\Public') { 17 | value = option.arg.public || option.def.default; 18 | 19 | if (!value) return; 20 | const fullpath = path.isAbsolute(value) ? value : path.join(process.cwd(), value); 21 | return path.normalize(fullpath); 22 | } 23 | return value; 24 | } 25 | }, 26 | env: { 27 | default: 'development', 28 | enum: ['production', 'test', 'development'], 29 | arg: false, 30 | env: 'NODE_ENV' 31 | }, 32 | engine: { 33 | default: 'pug', 34 | type: [String, Object] 35 | }, 36 | views: { 37 | default: 'views', 38 | type: String, 39 | folder: true 40 | }, 41 | secret: { 42 | default: 'secret-' + token, 43 | type: String, 44 | arg: false 45 | // TODO: integrate this 46 | // if (options.secret === 'your-random-string-here') { 47 | // throw new ServerError('/server/options/secret/example'); 48 | // } 49 | // 50 | // if (/^secret-/.test(options.secret) && options.verbose) { 51 | // console.log(new ServerError('/server/options/secret/generated')); 52 | // } 53 | }, 54 | 'x-powered-by': { 55 | default: false 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /docs/index.html.pug: -------------------------------------------------------------------------------- 1 | extends _layout/index 2 | 3 | block content 4 | .hero 5 | .content 6 | h1 7 | strong server.js 8 | | for Node 9 | div 10 | pre.sub npm install server 11 | a.button.docs(href='/documentation') 12 | | Documentation 13 | span.arrow 〉 14 | a.button.docs(href='/tutorials') 15 | | Tutorials 16 | span.arrow 〉 17 | 18 | // Just having some fun (; 19 | button.secret super secret button 20 | 21 | 22 | article.home 23 | div.features 24 | div.flex.two.three-600 25 | div 26 | img(src="/img/battery.svg") 27 | h2 Batteries on 28 | p Everything you need is loaded by default 29 | div 30 | img(src="/img/socketio.svg") 31 | h2 socket.io 32 | p Realtime channels in a couple of lines 33 | div 34 | img(src="/img/modern.svg") 35 | h2 Modern ES7+ 36 | p Use async/await and forget Callback Hell 37 | div 38 | img(src="/img/document.svg") 39 | h2 Documented 40 | p Many tutorials and docs for productivity 41 | div 42 | img(src="/img/lock.svg") 43 | h2 Secure 44 | p Sane defaults & great libraries underneath 45 | div 46 | img(src="/img/lego.svg") 47 | h2 Extensible 48 | p Fully compatible with Express middleware 49 | 50 | include:marked:noheader ../README.md 51 | 52 | 53 | script. 54 | function $(sel) { return document.querySelector(sel); } 55 | $('button.secret').onclick = function () { 56 | $('body').classList.toggle('liftoff'); 57 | } 58 | -------------------------------------------------------------------------------- /plugins/security/index.js: -------------------------------------------------------------------------------- 1 | const modern = require('../../src/modern'); 2 | const csurf = require('csurf'); 3 | const helmet = require('helmet'); 4 | 5 | module.exports = { 6 | name: 'security', 7 | options: { 8 | csrf: { 9 | env: 'SECURITY_CSRF', 10 | default: {}, 11 | type: Object 12 | }, 13 | contentSecurityPolicy: { 14 | env: 'SECURITY_CONTENTSECURITYPOLICY' 15 | }, 16 | expectCt: { 17 | env: 'SECURITY_EXPECTCT' 18 | }, 19 | dnsPrefetchControl: { 20 | env: 'SECURITY_DNSPREFETCHCONTROL' 21 | }, 22 | frameguard: { 23 | env: 'SECURITY_FRAMEGUARD' 24 | }, 25 | hidePoweredBy: { 26 | env: 'SECURITY_HIDEPOWEREDBY' 27 | }, 28 | hpkp: { 29 | env: 'SECURITY_HPKP' 30 | }, 31 | hsts: { 32 | env: 'SECURITY_HSTS' 33 | }, 34 | ieNoOpen: { 35 | env: 'SECURITY_IENOOPEN' 36 | }, 37 | noCache: { 38 | env: 'SECURITY_NOCACHE' 39 | }, 40 | noSniff: { 41 | env: 'SECURITY_NOSNIFF' 42 | }, 43 | referrerPolicy: { 44 | env: 'SECURITY_REFERRERPOLICY' 45 | }, 46 | xssFilter: { 47 | env: 'SECURITY_XSSFILTER' 48 | } 49 | }, 50 | before: [ 51 | ctx => ctx.options.security && ctx.options.security.csrf 52 | ? modern(csurf(ctx.options.security.csrf))(ctx) 53 | : false, 54 | ctx => { 55 | // Set the csrf for render(): https://expressjs.com/en/api.html#res.locals 56 | if (ctx.req.csrfToken) { 57 | ctx.csrf = ctx.req.csrfToken(); 58 | ctx.res.locals.csrf = ctx.csrf; 59 | } 60 | }, 61 | ctx => ctx.options.security ? modern(helmet(ctx.options.security))(ctx) : false 62 | ] 63 | }; 64 | -------------------------------------------------------------------------------- /plugins/express/integration.test.js: -------------------------------------------------------------------------------- 1 | const server = require('../../server'); 2 | const { status } = server.reply; 3 | 4 | // Test runner: 5 | const run = require('server/test/run'); 6 | 7 | describe('express', () => { 8 | it('is defined', () => { 9 | server(parseInt(1000 + Math.random() * 10000)).then(ctx => { 10 | expect(ctx.app).toBeDefined(); 11 | ctx.close(); 12 | }); 13 | }); 14 | 15 | it('accepts the options', async () => { 16 | 17 | const options = { 18 | 'case sensitive routing': true, 19 | 'etag': 'strong', 20 | 'jsonp callback name': 'abc', 21 | 'subdomain offset': 1, 22 | 'trust proxy': true, 23 | 'view cache': true, 24 | 'x-powered-by': false 25 | }; 26 | 27 | const res = await run({ express: options }, ctx => { 28 | for (let key in options) { 29 | expect(ctx.app.get(key)).toBe(options[key]); 30 | } 31 | return status(200); 32 | }).get('/'); 33 | expect(res.status).toBe(200); 34 | expect(res.body).toBe(''); 35 | }); 36 | 37 | it('ignores the view engine (use .engine instead)', async () => { 38 | const res = await run({ express: { 'view engine': 'abc' } }, ctx => { 39 | expect(ctx.app.get('env')).toBe('test'); 40 | expect(ctx.app.get('view engine')).toBe('pug'); 41 | return status(200); 42 | }).get('/'); 43 | expect(res.status).toBe(200); 44 | expect(res.body).toBe(''); 45 | }); 46 | 47 | it.skip('uses an engine', async () => { 48 | const res = run({ 49 | express: { engine: { 50 | blabla: 'I do not know how to make an engine yet' 51 | }} 52 | }).get('/'); 53 | expect(res.status).toBe(200); 54 | expect(res.body).toBe(''); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /plugins/log/integration.test.js: -------------------------------------------------------------------------------- 1 | const server = require('../../server'); 2 | const { status } = server.reply; 3 | const ConfigError = require('server/src/config/errors'); 4 | 5 | // Test runner: 6 | const run = require('server/test/run'); 7 | 8 | describe('log()', () => { 9 | it('is defined', () => { 10 | server(parseInt(1000 + Math.random() * 10000)).then(ctx => { 11 | expect(ctx.log).toBeDefined(); 12 | ctx.close(); 13 | }); 14 | }); 15 | 16 | it('is inside the middleware', async () => { 17 | const res = await run(ctx => status(ctx.log ? 200 : 500)).get('/'); 18 | expect(res.statusCode).toBe(200); 19 | }); 20 | 21 | it('has the right methods', async () => { 22 | const res = await run(ctx => { 23 | expect(ctx.log.emergency).toBeDefined(); 24 | expect(ctx.log.alert).toBeDefined(); 25 | expect(ctx.log.critical).toBeDefined(); 26 | expect(ctx.log.error).toBeDefined(); 27 | expect(ctx.log.warning).toBeDefined(); 28 | expect(ctx.log.notice).toBeDefined(); 29 | expect(ctx.log.info).toBeDefined(); 30 | expect(ctx.log.debug).toBeDefined(); 31 | return status(200); 32 | }).get('/'); 33 | expect(res.statusCode).toBe(200); 34 | }); 35 | 36 | it('rejects invalid log levels', async () => { 37 | const res = run({ log: 'abc' }).get('/'); 38 | 39 | // Now errors must be fully qualified with Jest 40 | expect(res).rejects.toMatchObject( 41 | new ConfigError('enum', { 42 | name: 'level', 43 | value: 'abc', 44 | possible: [ 45 | 'debug', 46 | 'info', 47 | 'notice', 48 | 'warning', 49 | 'error', 50 | 'critical', 51 | 'alert', 52 | 'emergency' 53 | ], 54 | status: 500 55 | }) 56 | ); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/join/index.js: -------------------------------------------------------------------------------- 1 | const load = require('loadware'); 2 | const assert = require('assert'); 3 | const reply = require('../../reply'); 4 | 5 | // Recursively resolve possible function returns 6 | const processReturn = (ctx, ret) => { 7 | if (!ret) return; 8 | 9 | // Use the returned reply instance 10 | if (ret.constructor.name === 'Reply') { 11 | return ret.exec(ctx); 12 | } 13 | 14 | // TODO: make a check for only accepting the right types of return values 15 | 16 | // Create a whole new reply thing 17 | const fn = typeof ret === 'number' ? 'status' : 'send'; 18 | return reply[fn](ret).exec(ctx); 19 | }; 20 | 21 | // Pass an array of modern middleware and return a single modern middleware 22 | module.exports = (...middles) => { 23 | 24 | // Flattify all of the middleware 25 | const middle = load(middles); 26 | 27 | // Go through each of them 28 | return async ctx => { 29 | for (const mid of middle) { 30 | try { 31 | if (ctx.req.solved) return; 32 | 33 | // DO NOT MERGE; the else is relevant only for ctx.error 34 | if (ctx.error) { 35 | // See if this middleware can fix it 36 | if (mid.error) { 37 | assert(mid.error instanceof Function, 'Error handler should be a function'); 38 | const ret = await mid.error(ctx); 39 | await processReturn(ctx, ret); 40 | if (ctx.res.headersSent) { 41 | ctx.req.solved = true; 42 | } 43 | } 44 | } 45 | // No error, call next middleware. Skips middleware if there's an error 46 | else { 47 | const ret = await mid(ctx); 48 | await processReturn(ctx, ret); 49 | if (ctx.res.headersSent) { 50 | ctx.req.solved = true; 51 | } 52 | } 53 | } catch (err) { 54 | ctx.error = err; 55 | } 56 | } 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /plugins/socket/index.js: -------------------------------------------------------------------------------- 1 | // Create a socket plugin 2 | const socketIO = require('socket.io'); 3 | 4 | const listeners = {}; 5 | 6 | module.exports = { 7 | name: 'socket', 8 | options: { 9 | path: { 10 | env: 'SOCKET_PATH' 11 | }, 12 | serveClient: {}, 13 | adapter: {}, 14 | origins: {}, 15 | parser: {}, 16 | pingTimeout: {}, 17 | pingInterval: {}, 18 | upgradeTimeout: {}, 19 | maxHttpBufferSize: {}, 20 | allowRequest: {}, 21 | transports: {}, 22 | allowUpgrades: {}, 23 | perMessageDeflate: {}, 24 | httpCompression: {}, 25 | cookie: {}, 26 | cookiePath: {}, 27 | cookieHttpOnly: {}, 28 | wsEngine: {}, 29 | cors: {}, 30 | }, 31 | router: (path, middle) => { 32 | listeners[path] = listeners[path] || []; 33 | listeners[path].push(middle); 34 | }, 35 | launch: ctx => { 36 | if (!ctx.options.socket) return; 37 | if (listeners.ping) { 38 | ctx.log.warning('socket("ping") has a special meaning, please avoid it'); 39 | } 40 | ctx.io = socketIO(ctx.server, ctx.options.socket); 41 | ctx.io.on('connect', socket => { 42 | // Create a new context assigned to each connected socket 43 | const createContext = extra => { 44 | return Object.assign({}, socket.client.request, ctx, extra); 45 | }; 46 | 47 | // Attach a `socket.on('name', cb)` to each of the callbacks 48 | for (let path in listeners) { 49 | if (path !== 'connect') { 50 | listeners[path].forEach(cb => { 51 | socket.on(path, data => cb(createContext({ path, socket, data }))); 52 | }); 53 | } 54 | } 55 | 56 | // This is not a callback and should be called straight away since we are 57 | // already inside `io.on('connect')` 58 | const path = 'connect'; 59 | if (listeners['connect']) { 60 | listeners[path].forEach(cb => cb(createContext({ path, socket }))); 61 | } 62 | }); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /docs/tutorials/index.html.pug: -------------------------------------------------------------------------------- 1 | extends ./tutorial 2 | 3 | block title 4 | +title('Tutorials') 5 | 6 | block description 7 | +description('Discover a world of tutorials for server.js with many Node.js exercises.') 8 | 9 | block article 10 | h1 Tutorials 11 | 12 | p Create awesome things with server.js. A collection of practical examples, from small tweaks to server.js to fully working webapps. Tutorials available: 13 | 14 | div.card 15 | header 16 | h2 Getting started 17 | footer 18 | p Get started by creating a Node.js project from scratch and get started. 19 | a.button(href="/tutorials/getting-started/") Read tutorial 20 | 21 | div.card 22 | header 23 | h2 Sessions in production 24 | footer 25 | p Learn how to set-up the session for production using Redis or other available datastores so there's persistence even after a server restart. 26 | a.button(href="/tutorials/sessions-production/") Read tutorial 27 | 28 | div.card 29 | header 30 | h2 TO-DO list 31 | footer 32 | p Simple TO-DO website using jQuery for the AJAX. Define an API to create, read, update and delete items from a MongoDB database. 33 | a.button(href="/tutorials/todo/") Read tutorial 34 | | 35 | a.button(href="https://github.com/franciscop/server-tutorial-todo") Source code 36 | 37 | div.card 38 | header 39 | h2 Spreadsheets data 40 | footer 41 | p Take a Google Spreadsheet and convert it into a read-only database for Node.js. 42 | a.button(href="/tutorials/spreadsheet/") Read tutorial 43 | | 44 | a.pseudo.button(href="https://github.com/franciscop/server-tutorial-spreadsheet") Source code 45 | 46 | div.card 47 | header 48 | h2 Realtime chat 49 | footer 50 | p Create a realtime chat with socket.io. You will choose a username on launch and then write to everyone reading the chat. 51 | a.button(href="/tutorials/chat/") Read tutorial 52 | | 53 | a.pseudo.button(href="https://github.com/franciscop/tokyochat") Source code 54 | 55 | -------------------------------------------------------------------------------- /src/modern/errors.js: -------------------------------------------------------------------------------- 1 | const error = require('../../error')('/server/modern/', { 2 | url: ({ id }) => `https://serverjs.io/documentation/errors/#${id}` 3 | }); 4 | 5 | error.missingmiddleware = ` 6 | modern() expects a middleware to be passed but nothing was passed. 7 | `; 8 | 9 | // error.MissingMiddleware = () => ` 10 | // modern() expects a middleware to be passed but nothing was passed. 11 | //`; 12 | 13 | 14 | error.invalidmiddleware = ({ type }) => ` 15 | modern() expects the argument to be a middleware function. 16 | "${type}" was passed instead 17 | `; 18 | 19 | // error.InvalidMiddleware = ({ type }) => ` 20 | // modern() expects the argument to be a middleware function. 21 | // "${type}" was passed instead 22 | // `; 23 | 24 | 25 | error.errormiddleware = ` 26 | modern() cannot create a modern middleware that handles errors. 27 | If you can handle an error in your middleware do it there. 28 | Otherwise, use ".catch()" for truly fatal errors as "server().catch()". 29 | `; 30 | 31 | // error.ErrorMiddleware = () => ` 32 | // modern() cannot create a modern middleware that handles errors. 33 | // If you can handle an error in your middleware do it there. 34 | // Otherwise, use ".catch()" for truly fatal errors as "server().catch()". 35 | // `; 36 | 37 | 38 | error.missingcontext = ` 39 | There is no context being passed to the middleware. 40 | `; 41 | 42 | // error.MissingContext = () => ` 43 | // There is no context being passed to the middleware. 44 | // `; 45 | 46 | 47 | error.malformedcontext = ({ item }) => ` 48 | The argument passed as context is malformed. 49 | Expecting it to be an object containing "${item}". 50 | This is most likely an error from "server.modern". 51 | Please report it: https://github.com/franciscop/server/issues 52 | `; 53 | 54 | // error.MalformedContext = ({ item }) => ` 55 | // The argument passed as context is malformed. 56 | // Expecting it to be an object containing "${item}". 57 | // This is most likely an error from "server.modern". 58 | // Please report it: https://github.com/franciscop/server/issues 59 | // `; 60 | 61 | 62 | module.exports = error; 63 | -------------------------------------------------------------------------------- /docs/assets/_prism.scss: -------------------------------------------------------------------------------- 1 | pre code { 2 | color:#000; 3 | background:0 0; 4 | text-shadow:0 1px #fff; 5 | font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace; 6 | text-align:left; 7 | white-space:pre; 8 | word-spacing:normal; 9 | word-break:normal; 10 | word-wrap:normal; 11 | line-height:1.5; 12 | -moz-tab-size:4; 13 | -o-tab-size:4; 14 | tab-size:4; 15 | -webkit-hyphens:none; 16 | -moz-hyphens:none; 17 | -ms-hyphens:none; 18 | hyphens:none 19 | } 20 | code ::-moz-selection, 21 | code::-moz-selection, 22 | pre ::-moz-selection, 23 | pre::-moz-selection{ 24 | text-shadow:none; 25 | background:#b3d4fc 26 | } 27 | code ::selection, 28 | code::selection, 29 | pre ::selection, 30 | pre::selection{ 31 | text-shadow:none; 32 | background:#b3d4fc 33 | } 34 | @media print{ 35 | code, 36 | pre{ 37 | text-shadow:none 38 | } 39 | } 40 | pre{ 41 | padding:1em; 42 | margin:.5em 0; 43 | overflow:auto 44 | } 45 | :not(pre)>code, 46 | pre{ 47 | background:#f5f2f0 48 | } 49 | :not(pre)>code{ 50 | padding:.1em; 51 | border-radius:.3em; 52 | white-space:normal 53 | } 54 | .token.cdata,.token.comment,.token.doctype,.token.prolog{ 55 | color:#708090 56 | } 57 | .token.punctuation{ 58 | color:#999 59 | } 60 | .namespace{ 61 | opacity:.7 62 | } 63 | .token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{ 64 | color:#905 65 | } 66 | .token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{ 67 | color:#690 68 | } 69 | .language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{ 70 | color:#a67f59; 71 | background:hsla(0,0%,100%,.5) 72 | } 73 | .token.atrule,.token.attr-value,.token.keyword{ 74 | color:#07a 75 | } 76 | .token.function{ 77 | color:#DD4A68 78 | } 79 | .token.important,.token.regex,.token.variable{ 80 | color:#e90 81 | } 82 | .token.bold,.token.important{ 83 | font-weight:700 84 | } 85 | .token.italic{ 86 | font-style:italic 87 | } 88 | .token.entity{ 89 | cursor:help 90 | } 91 | -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | You are welcome to contribute to the project. We use Grunt for building the website and jest for testing. 4 | 5 | 6 | ## Help wanted 7 | 8 | While you can open an issue for other reasons, these are the main areas where help is most needed: 9 | 10 | - Documentation. Writing new areas ([open an issue first](#pull-requests)), improving the grammar/typos/etc. Also, adding many examples, which should be tested first; a good place would be in `/examples`. 11 | 12 | - Testing. In many situations we are testing the *happy path* only, so we'd need more tests for others. 13 | 14 | - Errors. This project has an unusual section called errors where all of the errors are logged. Wording/improving these is very welcome. 15 | 16 | - Examples. Some simple examples in the folder `/examples` help newcomers and can be included later on in the documentation. The reverse is also true, you can test an example from the documentation in this folder. 17 | 18 | 19 | 20 | ## Pull Requests 21 | 22 | Before opening any PR with new functionality or change a section of the web or documentation please open an issue to discuss it first. 23 | 24 | > Tiny PR fixing typos, some wording, a website issue, etc are perfectly okay and promptly accepted. 25 | 26 | **Small PR are preferred** than large ones. If in doubt, open an issue, [ask me personally](http://francisco.io/) or make it small. Since this is mostly a 1-person project, large PR will probably sit idle for a while or flatly rejected (related: [you can also donate or sponsor the project](https://serverjs.io/sponsor) so I'll be able to invest more time and resources on it). 27 | 28 | 29 | 30 | ## Security issues 31 | 32 | Please do not open any vulnerability issue on Github's issue tracker. Send me an email (find my personal email in [my website](http://francisco.io/)) if you find a particular issue within server, or contact privately the package author in case it's a sub-package issue. 33 | 34 | I do not have a PGP-based email right now, but if requested I'll make sure to find out how to use it and publish it in [github](https://github.com/franciscop) or [twitter](http://twitter.com/fpresencia). 35 | -------------------------------------------------------------------------------- /src/join/integration.test.js: -------------------------------------------------------------------------------- 1 | // Test runner: 2 | const run = require('server/test/run'); 3 | 4 | const server = require('server'); 5 | const { get } = server.router; 6 | const nocsrf = { security: false }; 7 | 8 | describe('join', () => { 9 | 10 | it('loads as a function', async () => { 11 | const res = await run(() => 'Hello 世界').get('/'); 12 | expect(res.body).toBe('Hello 世界'); 13 | }); 14 | 15 | it('loads as an array', async () => { 16 | const res = await run([() => 'Hello 世界']).get('/'); 17 | expect(res.body).toBe('Hello 世界'); 18 | }); 19 | 20 | it('has a valid context', async () => { 21 | const res = await run(ctx => { 22 | return `${!!ctx.req}:${!!ctx.res}:${!!ctx.options}`; 23 | }).get('/'); 24 | expect(res.body).toBe('true:true:true'); 25 | }); 26 | 27 | it('loads as a relative file', async () => { 28 | const res = await run('./test/a.js').get('/'); 29 | expect(res.body).toBe('世界'); 30 | }); 31 | }); 32 | 33 | 34 | 35 | 36 | describe('Full trip request', () => { 37 | it('can perform a simple get', async () => { 38 | const res = await run(() => 'Hello 世界').get('/'); 39 | expect(res.body).toBe('Hello 世界'); 40 | }); 41 | 42 | it('uses the first reply', async () => { 43 | const res = await run([() => 'Hello 世界', () => 'Hello mundo']).get('/'); 44 | expect(res.body).toBe('Hello 世界'); 45 | }); 46 | 47 | it('loads as an array', async () => { 48 | const res = await run([() => 'Hello 世界']).get('/'); 49 | expect(res.body).toBe('Hello 世界'); 50 | }); 51 | 52 | it('can perform a simple post', async () => { 53 | const replyBody = ctx => `Hello ${ctx.data.a}`; 54 | const res = await run(nocsrf, replyBody).post('/', { body: { a: '世界' } }); 55 | expect(res.body).toBe('Hello 世界'); 56 | }); 57 | 58 | it('can set headers', async () => { 59 | const middle = get('/', ctx => { 60 | ctx.res.header('Expires', 12345); 61 | return 'Hello 世界'; 62 | }); 63 | const res = await run(middle).get('/'); 64 | expect(res.request.method).toBe('GET'); 65 | expect(res.headers.expires).toBe('12345'); 66 | expect(res.body).toBe('Hello 世界'); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/examples.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs=require('mz/fs'); 3 | 4 | function fromDir(startPath, filter, cb){ 5 | if (!fs.existsSync(startPath)) return; 6 | var files=fs.readdirSync(startPath); 7 | for(var i=0;i=0) { 14 | cb(filename); 15 | }; 16 | }; 17 | }; 18 | 19 | const getExamples = async () => { 20 | let matches = []; 21 | let files = []; 22 | fromDir(process.cwd() + '/docs', '.md', file => { files.push(file); }); 23 | for (let file of files) { 24 | let text = await fs.readFile(file, 'utf-8'); 25 | text.replace(/```[a-z]*\n[\s\S]*?\n```/g, code => { 26 | if (/\/\* ?test ?\*\//i.test(code)) { 27 | matches.push(code.split('\n').slice(1, -1).map(one => ' ' + one).join('\n')); 28 | } 29 | }); 30 | } 31 | return matches; 32 | }; 33 | 34 | 35 | 36 | describe('fn', () => { 37 | it('loads', async () => { 38 | const matches = await getExamples(); 39 | // console.log('Total tests found:', matches.length); 40 | for (let i in matches) { 41 | const filename = `${process.cwd()}/test/examples/test-${i}.test.js`; 42 | let code = matches[i]; 43 | const content = `// Test automatically retrieved. Do not edit manually 44 | const { render, json } = require('server/reply'); 45 | const { get, post } = require('server/router'); 46 | const { modern } = require('server').utils; 47 | const run = require('server/test/run'); 48 | const fs = require('mz/fs'); 49 | const path = require('path'); 50 | 51 | describe('Automatic test from content ${i}', () => { 52 | it('works', async () => { 53 | // START 54 | ${matches[i]} 55 | // END 56 | }); 57 | }); 58 | `; 59 | try { 60 | const retrieved = await fs.readFile(filename, 'utf-8'); 61 | if (retrieved !== content) { 62 | await fs.writeFile(filename, content, 'utf-8'); 63 | } 64 | } catch (err) { 65 | if (err.code === 'ENOENT') { 66 | await fs.writeFile(filename, content, 'utf-8'); 67 | } else { 68 | console.log(err); 69 | } 70 | } 71 | // console.log(content); 72 | } 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /docs/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | http://serverjs.io/daily1.00 4 | https://serverjs.iodaily1.00 5 | https://serverjs.io/tutorialsdaily0.85 6 | https://serverjs.io/documentationdaily0.85 7 | https://serverjs.io/tutorials/getting-started/daily0.85 8 | https://serverjs.io/documentation/daily0.85 9 | https://serverjs.io/sponsordaily0.85 10 | https://serverjs.io/documentation/optionsdaily0.69 11 | https://serverjs.io/documentation/middlewaredaily0.69 12 | https://serverjs.io/documentation/routerdaily0.69 13 | https://serverjs.io/tutorials/daily0.69 14 | https://serverjs.io/sponsor/daily0.69 15 | https://serverjs.io/tutorials/getting-starteddaily0.56 16 | https://serverjs.io/tutorials/spreadsheetdaily0.56 17 | https://serverjs.io/documentation/options/daily0.56 18 | https://serverjs.io/documentation/middleware/daily0.56 19 | https://serverjs.io/documentation/router/daily0.56 20 | https://serverjs.io/documentation/advanced/daily0.56 21 | https://serverjs.io/tutorials/spreadsheet/daily0.46 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/config/errors.js: -------------------------------------------------------------------------------- 1 | const error = require('../../error')('/server/options', { 2 | url: ({ id }) => `https://serverjs.io/documentation/errors/#${id}` 3 | }); 4 | 5 | error.notobject = ` 6 | Your options must be an object as required by the options definition. 7 | If you are a developer and want to accept a single option, make sure 8 | to use the '__root' property. 9 | `; 10 | 11 | error.noarg = ({ name }) => ` 12 | The option '${name}' cannot be passed through the arguments of server. This 13 | might be because it's sensitive and it has to be set in the environment. 14 | Please read the documentation for '${name}' and make sure to set it correctly. 15 | `; 16 | 17 | error.noenv = ({ name }) => ` 18 | The option '${name}' cannot be passed through the environment of server. 19 | Please read the documentation for '${name}' and make sure to set it correctly. 20 | `; 21 | 22 | error.cannotextend = ({ type, name }) => ` 23 | The option "${name}" must be an object but it received "${type}". 24 | Please check your options to make sure you are passing an object. 25 | ${type === 'undefined' ? ` 26 | If you are the creator of the plugin and you are receiving 'undefined', you 27 | could allow for the default behaviour to be an empty object 'default: {}' 28 | ` : ''} 29 | `; 30 | 31 | error.required = ({ name }) => ` 32 | The option '${name}' is required but it was not set neither as an argument nor 33 | in the environment. Please make sure to set it. 34 | `; 35 | 36 | error.type = ({ name, expected, received, value }) => ` 37 | The option '${name}' should be a '[${typeof expected}]' but you passed a '${received}': 38 | ${JSON.stringify(value)} 39 | `; 40 | 41 | error.enum = ({ name, value, possible }) => ` 42 | The option '${name}' has a value of '${value}' but it should have one of these values: 43 | ${JSON.stringify(possible)} 44 | `; 45 | 46 | error.validate = ({ name, value }) => ` 47 | Failed to validate the option '${name}' with the value '${value}'. Please 48 | consult this option documentation for more information. 49 | `; 50 | 51 | error.secretexample = ` 52 | It looks like you are trying to use 'your-random-string-here' as the secret, 53 | just as in the documentation. Please don't do this! Create a strong secret 54 | and store it in your '.env'. 55 | `; 56 | 57 | error.secretgenerated = ` 58 | Please change the secret in your environment configuration. 59 | The default one is not recommended and should be changed. 60 | More info in https://serverjs.io/errors#defaultSecret 61 | `; 62 | 63 | module.exports = error; 64 | -------------------------------------------------------------------------------- /plugins/express/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const hbs = require('hbs'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | name: 'express', 7 | options: { 8 | // See these in-depth in https://expressjs.com/en/api.html#app.set 9 | 'case sensitive routing': {}, 10 | 'env': { 11 | inherit: 'env' 12 | }, 13 | 'etag': {}, 14 | 'jsonp callback name': {}, 15 | 'json replacer': {}, 16 | 'json spaces': {}, 17 | 'query parser': {}, 18 | 'strict routing': {}, 19 | 'subdomain offset': {}, 20 | 'trust proxy': {}, 21 | 'views': { 22 | default: 'views', 23 | inherit: true, 24 | type: String, 25 | folder: true 26 | }, 27 | 'view cache': {}, 28 | 'view engine': { 29 | inherit: 'engine' 30 | }, 31 | 'x-powered-by': {} 32 | }, 33 | init: async ctx => { 34 | ctx.express = express; 35 | ctx.app = ctx.express(); 36 | 37 | // Go through all of the options and set the right ones 38 | for (let key in ctx.options.express) { 39 | let value = ctx.options.express[key]; 40 | if (typeof value !== 'undefined') { 41 | ctx.app.set(key, value); 42 | } 43 | } 44 | 45 | // Add the views into the core 46 | if (path.resolve(ctx.options.views) === path.resolve(process.cwd())) { 47 | throw new Error( 48 | 'The "views" option should point to a subfolder of the project and not the root of it' 49 | ); 50 | } 51 | await new Promise(function(resolve, reject) { 52 | hbs.registerPartials(ctx.options.views, function(err) { 53 | if (err) reject(err); 54 | resolve(); 55 | }); 56 | }); 57 | 58 | // Accept HTML as a render extension 59 | ctx.app.engine('html', hbs.__express); 60 | 61 | if (ctx.options.engine) { 62 | // If it's an object, expect a { engine: { engineName: engineFN } } 63 | if (typeof ctx.options.engine === 'object') { 64 | for (let name in ctx.options.engine) { 65 | ctx.app.engine(name, ctx.options.engine[name]); 66 | ctx.app.set('view engine', name); 67 | } 68 | } else { // Simple case like { engine: 'pug' } 69 | ctx.app.set('view engine', ctx.options.engine); 70 | } 71 | } 72 | }, 73 | listen: ctx => new Promise((resolve, reject) => { 74 | ctx.server = ctx.app.listen(ctx.options.port, () => { 75 | ctx.log.debug(`Server started on http://localhost:${ctx.options.port}/`); 76 | resolve(); 77 | }); 78 | ctx.close = () => new Promise((res, rej) => { 79 | ctx.server.close(err => err ? rej(err) : res()); 80 | }); 81 | ctx.server.on('error', err => reject(err)); 82 | }) 83 | }; 84 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // server for Node.js (https://serverjs.io/) 2 | // A simple and powerful server for Node.js. 3 | 4 | // Internal modules 5 | const config = require('./src/config'); 6 | const router = require('./router'); 7 | const reply = require('./reply'); 8 | const join = require('./src/join/index.js'); 9 | const modern = require('./src/modern'); 10 | 11 | // Create a context per-request 12 | const context = (self, req, res) => Object.assign(req, self, { req, res }); 13 | 14 | // Get the functions from the plugins for a special point 15 | const hook = (ctx, name) => ctx.plugins.map(p => p[name]).filter(p => p); 16 | 17 | 18 | 19 | // Main function 20 | const Server = async (...middle) => { 21 | 22 | // Initialize the global context 23 | const ctx = {}; 24 | 25 | // First parameter can be: 26 | // - options: Number || Object (cannot be ID'd) 27 | // - middleware: undefined || null || Boolean || Function || Array 28 | const opts = ( 29 | typeof middle[0] === 'undefined' || 30 | typeof middle[0] === 'boolean' || 31 | typeof middle[0] === 'string' || 32 | middle[0] === null || 33 | middle[0] instanceof Function || 34 | middle[0] instanceof Array 35 | ) ? {} : middle.shift(); 36 | 37 | // Set the options for the context of Server.js 38 | ctx.options = await config(opts, module.exports.plugins); 39 | 40 | // Only enabled plugins through the config 41 | ctx.plugins = module.exports.plugins.filter(p => ctx.options[p.name]); 42 | 43 | ctx.utils = { modern: modern }; 44 | ctx.modern = modern; 45 | 46 | // All the init beforehand 47 | for (let init of hook(ctx, 'init')) { 48 | await init(ctx); 49 | } 50 | 51 | 52 | 53 | // PLUGIN middleware 54 | ctx.middle = join(hook(ctx, 'before'), middle, hook(ctx, 'after')); 55 | 56 | // Main thing here 57 | ctx.app.use((req, res) => ctx.middle(context(ctx, req, res))); 58 | 59 | 60 | 61 | // Different listening methods 62 | await Promise.all(hook(ctx, 'listen').map(listen => listen(ctx))); 63 | 64 | // After launching it (already proxified) 65 | for (let launch of hook(ctx, 'launch')) { 66 | await launch(ctx); 67 | } 68 | 69 | return ctx; 70 | }; 71 | 72 | module.exports = Server; 73 | module.exports.router = router; 74 | module.exports.reply = reply; 75 | module.exports.utils = { 76 | modern: modern 77 | }; 78 | module.exports.plugins = [ 79 | require('./plugins/log'), 80 | require('./plugins/express'), 81 | require('./plugins/parser'), 82 | require('./plugins/static'), 83 | require('./plugins/socket'), 84 | require('./plugins/session'), 85 | require('./plugins/security'), 86 | require('./plugins/favicon'), 87 | require('./plugins/compress'), 88 | require('./plugins/final') 89 | ]; 90 | -------------------------------------------------------------------------------- /plugins/final/final.test.js: -------------------------------------------------------------------------------- 1 | const run = require('server/test/run'); 2 | 3 | // Note: the `raw` option only works for tests 4 | 5 | const storeLog = out => ({ 6 | report: log => { 7 | out.log = log.toString(); 8 | } 9 | }); 10 | 11 | describe('final', () => { 12 | it('gets called with an unhandled error', async () => { 13 | const simple = () => { 14 | throw new Error('Hello Error'); 15 | }; 16 | const out = {}; 17 | const res = await run({ raw: true, log: storeLog(out) }, simple).get('/'); 18 | expect(res.statusCode).toBe(500); 19 | expect(res.body).toBe('Internal Server Error'); 20 | expect(out.log).toMatch('Hello Error'); 21 | }); 22 | 23 | it('is not called if the previous one finishes', async () => { 24 | let called = false; 25 | const simple = () => { 26 | called = true; 27 | }; 28 | const out = {}; 29 | const res = await run( 30 | { raw: true, log: storeLog(out) }, 31 | () => 'Hello world', 32 | simple 33 | ).get('/'); 34 | expect(res.statusCode).toBe(200); 35 | expect(res.body).toBe('Hello world'); 36 | expect(called).toBe(false); 37 | }); 38 | 39 | it('displays the appropriate error to the public', async () => { 40 | const simple = () => { 41 | const err = new Error('Hello Error: display to the public'); 42 | err.public = true; 43 | throw err; 44 | }; 45 | const out = {}; 46 | const res = await run({ raw: true, log: storeLog(out) }, simple).get('/'); 47 | expect(res.statusCode).toBe(500); 48 | expect(res.body).toBe('Hello Error: display to the public'); 49 | expect(out.log).toMatch('Hello Error'); 50 | }); 51 | 52 | it('makes the status 500 if it is invalid', async () => { 53 | const simple = () => { 54 | const err = new Error('Hello Error'); 55 | err.status = 'pepito'; 56 | throw err; 57 | }; 58 | const out = {}; 59 | const res = await run({ raw: true, log: storeLog(out) }, simple).get('/'); 60 | expect(res.statusCode).toBe(500); 61 | expect(res.body).toBe('Internal Server Error'); 62 | expect(out.log).toMatch('Hello Error'); 63 | }); 64 | 65 | it('does not reply if the headers are already sent', async () => { 66 | const simple = ctx => { 67 | ctx.res.send('Error 世界'); 68 | throw new Error('Hello'); 69 | }; 70 | const res = await run(simple).get('/'); 71 | expect(res.body).toBe('Error 世界'); 72 | }); 73 | 74 | it('handles non-existing requests to a 404', async () => { 75 | const out = {}; 76 | const res = await run({ log: storeLog(out) }).get('/non-existing'); 77 | 78 | expect(res.statusCode).toBe(404); 79 | expect(out.log).toMatch(/did not return anything/); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.42", 4 | "description": "A modern and powerful server for Node.js", 5 | "homepage": "https://serverjs.io/", 6 | "repository": "https://github.com/franciscop/server.git", 7 | "bugs": "https://github.com/franciscop/server/issues", 8 | "funding": "https://www.paypal.me/franciscopresencia/19", 9 | "author": "Francisco Presencia (https://francisco.io/)", 10 | "license": "MIT", 11 | "scripts": { 12 | "start": "node .", 13 | "dev": "grunt watch", 14 | "build": "grunt", 15 | "pretest": "cp .env.demo .env", 16 | "test": "jest --coverage --forceExit", 17 | "kill-comment": "Kill a process running in a specific port: PORT=3000 npm run kill", 18 | "kill": "kill $(lsof -t -i:$PORT) && echo '> KILLED!' || echo '> The port was already dead'" 19 | }, 20 | "keywords": [ 21 | "server", 22 | "node.js", 23 | "http", 24 | "websocket", 25 | "socket", 26 | "async" 27 | ], 28 | "main": "server.js", 29 | "directories": { 30 | "Documentation": "./docs/documentation", 31 | "Code": "./src", 32 | "Plugins": "./plugins", 33 | "Examples": "./examples" 34 | }, 35 | "engines": { 36 | "node": ">=10.0.0" 37 | }, 38 | "engineStrict": true, 39 | "dependencies": { 40 | "body-parser": "^1.20.2", 41 | "compression": "^1.7.4", 42 | "connect-redis": "^7.1.1", 43 | "cookie-parser": "^1.4.6", 44 | "csurf": "^1.11.0", 45 | "dotenv": "^16.4.5", 46 | "express": "^4.18.3", 47 | "express-session": "^1.18.0", 48 | "extend": "^3.0.2", 49 | "hbs": "^4.2.0", 50 | "helmet": "^7.1.0", 51 | "ioredis": "^5.3.2", 52 | "loadware": "^2.0.0", 53 | "method-override": "^3.0.0", 54 | "mz": "^2.7.0", 55 | "npmlog": "^7.0.1", 56 | "path-to-regexp": "^6.2.1", 57 | "pug": "^3.0.2", 58 | "response-time": "^2.3.2", 59 | "serve-favicon": "^2.5.0", 60 | "serve-index": "^1.9.1", 61 | "socket.io": "^4.7.4", 62 | "upload-files-express": "^0.4.0" 63 | }, 64 | "devDependencies": { 65 | "eslint": "^8.57.0", 66 | "eslint-plugin-jasmine": "^4.1.3", 67 | "grunt": "^1.6.1", 68 | "grunt-bytesize": "^0.2.0", 69 | "grunt-contrib-connect": "^4.0.0", 70 | "grunt-contrib-jshint": "^3.2.0", 71 | "grunt-contrib-pug": "^3.0.0", 72 | "grunt-contrib-watch": "^1.1.0", 73 | "grunt-sass": "^3.1.0", 74 | "jest": "^29.7.0", 75 | "jest-jasmine2": "^29.7.0", 76 | "jstransformer-marked": "^1.4.0", 77 | "picnic": "^7.1.0", 78 | "request-promises": "^1.1.0", 79 | "sass": "^1.71.1", 80 | "supertest": "^6.3.4" 81 | }, 82 | "jest": { 83 | "transformIgnorePatterns": [ 84 | "/node_modules/" 85 | ], 86 | "testRunner": "jest-jasmine2" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/config/init.test.js: -------------------------------------------------------------------------------- 1 | const server = require('server'); 2 | const schema = require('./schema'); 3 | const port = require('server/test/port'); 4 | 5 | describe('Options', () => { 6 | it('default settings are correct', async () => { 7 | expect(schema.port.default).toBe(3000); 8 | expect(schema.engine.default).toBe('pug'); 9 | expect(schema.public.default).toBe('public'); 10 | expect(schema.secret.default).toMatch(/^secret-/); 11 | }); 12 | 13 | it('accepts a single port Number', async () => { 14 | const options = port(); 15 | const ctx = await server(options); 16 | ctx.close(); 17 | expect(ctx.options.port).toBe(options); 18 | }); 19 | 20 | it('accepts a simple Object with a port prop', async () => { 21 | const options = { port: port() }; 22 | const ctx = await server(options); 23 | ctx.close(); 24 | expect(ctx.options.port).toBe(options.port); 25 | }); 26 | 27 | // it('can listen only one time to the same port', async () => { 28 | // const onePort = port(); 29 | // const ctx = await server(onePort); 30 | // let err = await server(onePort).catch(err => err); 31 | // await ctx.close(); 32 | // expect(err.code).toBe('EADDRINUSE'); 33 | // }); 34 | 35 | it('sets the engine properly `engine`', async () => { 36 | const ctx = await server({ engine: 'whatever', port: port() }); 37 | ctx.close(); 38 | expect(ctx.app.get('view engine')).toBe('whatever'); 39 | }); 40 | 41 | // TODO: this goes in express plugin 42 | // it('sets the engine properly `view engine`', async () => { 43 | // const ctx = await server({ 'view engine': 'whatever', port: port() }); 44 | // ctx.close(); 45 | // expect(ctx.app.get('view engine')).toBe('whatever'); 46 | // }); 47 | 48 | it('has independent instances', async () => { 49 | const portA = port(); 50 | const portB = port(); 51 | const serv1 = await server(portA); 52 | const serv2 = await server(portB); 53 | await serv1.close(); 54 | await serv2.close(); 55 | 56 | expect(serv2.options.port).toBe(portB); 57 | const portC = port(); 58 | serv2.options.port = portC; 59 | expect(serv1.options.port).toBe(portA); 60 | expect(serv2.options.port).toBe(portC); 61 | 62 | serv2.a = 'abc'; 63 | expect(typeof serv1.a).toBe('undefined'); 64 | expect(serv2.a).toBe('abc'); 65 | }); 66 | 67 | // // NOT PART OF THE STABLE API 68 | // it('logs init string', async () => { 69 | // const logs = []; 70 | // const index = server.plugins.push({ 71 | // name: 'log', launch: ctx => { ctx.log = msg => logs.push(msg) } 72 | // }); 73 | // const ctx = await server({ port: port(), verbose: true }); 74 | // ctx.close(); 75 | // delete server.plugins[index]; 76 | // expect(logs.filter(one => /started on/.test(one)).length).toBe(1); 77 | // }); 78 | }); 79 | -------------------------------------------------------------------------------- /docs/assets/_article.scss: -------------------------------------------------------------------------------- 1 | article { 2 | width: 100%; 3 | max-width: 900px; 4 | margin: 20px auto 30vh; 5 | background: #fff; 6 | padding: 2em; 7 | 8 | &.documentation, 9 | &.tutorial { 10 | 11 | .main { 12 | max-width: 100%; 13 | } 14 | 15 | .source { 16 | float: right; 17 | margin: -5px; 18 | } 19 | 20 | @media all and (min-width: $picnic-breakpoint) { 21 | margin-top: 50px; 22 | 23 | .flex { 24 | width: 100%; 25 | margin: 0; 26 | } 27 | .toc { 28 | width: 28%; 29 | margin-right: 2%; 30 | } 31 | .main { 32 | width: 70%; 33 | } 34 | } 35 | } 36 | 37 | .features { 38 | margin: 20px 0 30px; 39 | text-align: center; 40 | 41 | img { 42 | margin: 10px auto -5px; 43 | width: 90px; 44 | display: block; 45 | filter: opacity(.8); 46 | } 47 | 48 | h2 { 49 | padding-top: 0; 50 | } 51 | 52 | p { 53 | margin: 0 auto; 54 | } 55 | 56 | @media all and (min-width: 600px) { 57 | img { 58 | margin: 0 auto; 59 | width: 100px; 60 | } 61 | p { 62 | width: 90%; 63 | margin: 0 auto; 64 | } 65 | } 66 | } 67 | 68 | img { 69 | max-width: 100%; 70 | } 71 | 72 | 73 | /* p only if it's the first elementc */ 74 | & > p:first-child { 75 | margin-top: 0; 76 | } 77 | 78 | /*tighten up*/ 79 | h1, 80 | h2, 81 | h3 { 82 | margin: 0; 83 | padding-top: 25px; 84 | padding-bottom: 0; 85 | 86 | a { 87 | color: inherit; 88 | } 89 | 90 | a:hover { 91 | color: #0074d9; 92 | } 93 | } 94 | 95 | .self { 96 | float: right; 97 | margin-right: 5px; 98 | } 99 | 100 | h1 { 101 | margin-top: -106px; 102 | padding-top: 100px; 103 | line-height: 1.1; 104 | } 105 | 106 | h1 + *, 107 | h2 + *, 108 | h3 + * { 109 | margin-top: .6em; 110 | } 111 | 112 | table { 113 | margin: 1.5em 0 .5em; 114 | width: 100%; 115 | max-width: 100%; 116 | 117 | td, th { 118 | padding: .3em .6em; 119 | } 120 | 121 | th { 122 | background: none; 123 | color: #333; 124 | } 125 | 126 | tr:nth-child(2n) { 127 | background: none; 128 | } 129 | 130 | th, 131 | td { 132 | border: 2px solid #ddd; 133 | } 134 | } 135 | 136 | .pages { 137 | text-align: left; 138 | } 139 | } 140 | 141 | 142 | 143 | // Responsive spacing 144 | @media all and (max-width: 900px) { 145 | article { 146 | padding: $rspacing; 147 | width: 100%; 148 | border-radius: 0; 149 | 150 | table { 151 | display: block; 152 | overflow-x: auto; 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /reply/unit.test.js: -------------------------------------------------------------------------------- 1 | const reply = require("."); 2 | 3 | describe("reply", () => { 4 | it("loads the main reply", () => { 5 | expect(JSON.stringify(reply)).toEqual( 6 | JSON.stringify(require("server").reply) 7 | ); 8 | expect(JSON.stringify(reply)).toEqual( 9 | JSON.stringify(require("server/reply")) 10 | ); 11 | }); 12 | 13 | it("has the right methods defined", () => { 14 | expect(reply.cookie).toEqual(jasmine.any(Function)); 15 | expect(reply.download).toEqual(jasmine.any(Function)); 16 | expect(reply.end).toEqual(jasmine.any(Function)); 17 | expect(reply.file).toEqual(jasmine.any(Function)); 18 | expect(reply.header).toEqual(jasmine.any(Function)); 19 | expect(reply.json).toEqual(jasmine.any(Function)); 20 | expect(reply.jsonp).toEqual(jasmine.any(Function)); 21 | expect(reply.redirect).toEqual(jasmine.any(Function)); 22 | expect(reply.render).toEqual(jasmine.any(Function)); 23 | expect(reply.send).toEqual(jasmine.any(Function)); 24 | expect(reply.status).toEqual(jasmine.any(Function)); 25 | expect(reply.type).toEqual(jasmine.any(Function)); 26 | }); 27 | 28 | it("can load all the methods manually", () => { 29 | expect(typeof require("server/reply/cookie")).toBe("function"); 30 | expect(typeof require("server/reply/download")).toBe("function"); 31 | expect(typeof require("server/reply/end")).toBe("function"); 32 | expect(typeof require("server/reply/file")).toBe("function"); 33 | expect(typeof require("server/reply/header")).toBe("function"); 34 | expect(typeof require("server/reply/json")).toBe("function"); 35 | expect(typeof require("server/reply/jsonp")).toBe("function"); 36 | expect(typeof require("server/reply/redirect")).toBe("function"); 37 | expect(typeof require("server/reply/render")).toBe("function"); 38 | expect(typeof require("server/reply/send")).toBe("function"); 39 | expect(typeof require("server/reply/status")).toBe("function"); 40 | expect(typeof require("server/reply/type")).toBe("function"); 41 | }); 42 | 43 | describe("reply: instances instead of global", () => { 44 | it("adds a method to the stack", () => { 45 | const mock = reply.file("./index.js"); 46 | expect(mock.stack.length).toEqual(1); 47 | const inst = reply.file("./index.js"); 48 | expect(inst.stack.length).toEqual(1); 49 | 50 | // Do not touch the global 51 | expect(mock.stack.length).toEqual(1); 52 | }); 53 | 54 | it("adds several methods correctly", () => { 55 | const mock = reply.file("./index.js"); 56 | expect(mock.stack.length).toEqual(1); 57 | const inst = reply.file("./index.js").file("./whatever.js"); 58 | expect(inst.stack.length).toEqual(2); 59 | 60 | // Do not touch the global 61 | expect(mock.stack.length).toEqual(1); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /docs/documentation/testing/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 |
4 | If you happen to stumble here, this bit of the documentation is outdated and follows some old code. Please help us improve the project and the docs so we can make it into the official release. 5 |
6 | 7 | There's a small test suite included, but you probably want to use something more specific to your use-case. 8 | 9 | Testing that a middleware correctly handles the lack of a user: 10 | 11 | ```js 12 | // auth/errors.js (more info in /documentation/errors/) 13 | const error = require('server/error'); 14 | error['/app/auth/nouser'] = 'You must be authenticated to do this'; 15 | module.exports = error; 16 | ``` 17 | 18 | Our main module: 19 | 20 | ```js 21 | // auth/needuser.js 22 | const AuthError = require('./errors'); 23 | 24 | module.exports = ctx => { 25 | if (!ctx.user) { 26 | throw new AuthError('/app/auth/nouser', { status: 403, public: true }); 27 | } 28 | }; 29 | ``` 30 | 31 | Then to test this module: 32 | 33 | ```js 34 | // auth/needuser.test.js 35 | const run = require('server/test/run'); 36 | const needuser = require('./needuser'); 37 | 38 | describe('auth/needuser.js', () => { 39 | it('returns a server error without a user', async () => { 40 | const res = await run(needuser).get('/'); 41 | expect(res.status).toBe(403); 42 | }); 43 | 44 | it('works with a mocked user', async () => { 45 | const mockuser = ctx => { ctx.user = {}; }; 46 | const res = await run(mockuser, needuser).get('/'); 47 | expect(res.status).toBe(200); 48 | }); 49 | }); 50 | ``` 51 | 52 | ## run() 53 | 54 | This function accepts the same arguments as `server()`, however it will return an API that you can use to test any middleware (and, by extension, any route) that you want. The API that it returns so far is this: 55 | 56 | ```js 57 | const run = require('server/test/run'); 58 | 59 | const api = run(TOTEST); 60 | 61 | api.get.then(res => { ... }); 62 | api.post.then(res => { ... }); 63 | api.put.then(res => { ... }); 64 | api.del.then(res => { ... }); 65 | ``` 66 | 67 | ## Disable CSRF 68 | 69 | For testing POST, PUT and DELETE methods you might want to disable CSRF. To do that, just pass it the appropriate option: 70 | 71 | ```js 72 | run({ security: { csrf: false } }, TOTEST); 73 | ``` 74 | 75 | This API accepts as arguments: 76 | 77 | ```js 78 | api.get(URL, OPTIONS); 79 | ``` 80 | 81 | It is using [`request`](https://github.com/request/request) underneath, so the options are the same as for this module. There are few small differences: 82 | 83 | - It will generate the port randomly from [1024](https://stackoverflow.com/q/413807/938236) to [49151](https://stackoverflow.com/a/113237/938236). However, there is a chance of collision that grows [faster than expected](https://en.wikipedia.org/wiki/Birthday_problem) as your number of tests grows. There's mitigation code going on to avoid collisions so until the tens of thousands of tests it should be fine. 84 | - The URLs will be made local internally to `http://localhost:${port}` unless you fully qualify them (which is not recommended). 85 | -------------------------------------------------------------------------------- /plugins/parser/index.js: -------------------------------------------------------------------------------- 1 | // Parser plugin 2 | // Get the raw request and transform it into something usable 3 | // Examples: ctx.body, ctx.files, etc 4 | const join = require('../../src/join'); 5 | const modern = require('../../src/modern'); 6 | 7 | const plugin = { 8 | name: 'parser', 9 | options: { 10 | body: { 11 | type: [Object, Boolean], 12 | default: { extended: true }, 13 | extend: true 14 | }, 15 | json: { 16 | type: [Object, Boolean], 17 | default: {} 18 | }, 19 | text: { 20 | type: Object, 21 | default: {} 22 | }, 23 | data: { 24 | type: Object, 25 | default: {} 26 | }, 27 | cookie: { 28 | type: Object, 29 | default: {} 30 | }, 31 | method: { 32 | type: [Object, String, Boolean], 33 | default: [ 34 | 'X-HTTP-Method', 35 | 'X-HTTP-Method-Override', 36 | 'X-Method-Override', 37 | '_method' 38 | ], 39 | // Coerce it into an Array if it is not already 40 | clean: value => typeof value === 'string' ? [value] : value 41 | } 42 | }, 43 | 44 | // It is populated in "init()" right now: 45 | before: [ 46 | ctx => { 47 | if (!ctx.options.parser.method) return; 48 | return join(ctx.options.parser.method.map(one => { 49 | return modern(require('method-override')(one)); 50 | }))(ctx); 51 | }, 52 | 53 | ctx => { 54 | if (!ctx.options.parser.body) return; 55 | const body = require('body-parser').urlencoded(ctx.options.parser.body); 56 | return modern(body)(ctx); 57 | }, 58 | 59 | // JSON parser 60 | ctx => { 61 | if (!ctx.options.parser.json) return; 62 | const json = require('body-parser').json(ctx.options.parser.json); 63 | return modern(json)(ctx); 64 | }, 65 | 66 | // Text parser 67 | ctx => { 68 | if (!ctx.options.parser.text) return; 69 | const text = require('body-parser').text(ctx.options.parser.text); 70 | return modern(text)(ctx); 71 | }, 72 | 73 | // Data parser 74 | ctx => { 75 | if (!ctx.options.parser.data) return; 76 | const data = require('upload-files-express')(ctx.options.parser.data); 77 | return modern(data)(ctx); 78 | }, 79 | 80 | // Cookie parser 81 | ctx => { 82 | if (!ctx.options.parser.cookie) return; 83 | const cookie = require('cookie-parser')( 84 | ctx.options.secret, 85 | ctx.options.parser.cookie 86 | ); 87 | return modern(cookie)(ctx); 88 | }, 89 | 90 | // Add a reference from ctx.req.body to the ctx.data and an alias 91 | ctx => { 92 | ctx.data = ctx.body; 93 | }, 94 | 95 | // Fix the IP for heroku and similars 96 | ctx => { 97 | const forwarded = ctx.headers['x-forwarded-for']; 98 | if (!forwarded) return; 99 | 100 | const ip = forwarded.trim().split(/,\s?/g).shift(); 101 | Object.defineProperty(ctx, 'ip', { enumerable: true, value: ip }); 102 | }, 103 | ] 104 | }; 105 | 106 | module.exports = plugin; 107 | -------------------------------------------------------------------------------- /docs/tutorials/sessions-production/README.md: -------------------------------------------------------------------------------- 1 | # Session in production 2 | 3 | Sessions work out of the box for developing, but they need a bit of extra work to be ready for production. 4 | 5 | ## Secret 6 | 7 | The first thing to change is adding a [session secret](https://martinfowler.com/articles/session-secret.html) as an [environment variable](/documentation/options/#environment) in `.env` for your machine: 8 | 9 | ``` 10 | SECRET=your-random-string-here 11 | ``` 12 | 13 | This will be used to secure the cookies as well as for other plugins that need a secret. Make it unique, long and random. Then **don't forget to add a different one for the production server** and other stages in your deploy pipeline if any. Also, exclude the `.env` file from Git [as explained here](http://localhost:3000/documentation/options/#environment). 14 | 15 | 16 | ## Storage 17 | 18 | **By default** [sessions work in-memory with *server*](https://github.com/expressjs/session) so they are [**not ready for production**](https://github.com/expressjs/session/pull/220): 19 | 20 | ```js 21 | // Simple visit counter for the main page 22 | const counter = get('/', ctx => { 23 | ctx.session.views = (ctx.session.views || 0) + 1; 24 | return { views: ctx.session.views }; 25 | }); 26 | 27 | /* test */ 28 | await run(counter).alive(async api => { 29 | let res = await api.get('/'); 30 | expect(res.body.views).toBe(1); 31 | res = await api.get('/'); 32 | expect(res.body.views).toBe(2); 33 | res = await api.get('/'); 34 | expect(res.body.views).toBe(3); 35 | }); 36 | ``` 37 | 38 | This works great for testing; for quick demos and for short sessions, but **all session data will die when the server is restarted** since they are stored in the RAM. 39 | 40 | To make them persistent we recommend [using a compatible session store](https://github.com/expressjs/session#compatible-session-stores). We bundle Redis for Node.js by default, so you just have to install it (\*nix systems have it easily available). For example, on Ubuntu: 41 | 42 | ``` 43 | sudo apt install redis-server 44 | ``` 45 | 46 | Then edit your `.env` to include `REDIS_URL`: 47 | 48 | ``` 49 | SECRET=your-random-string-here 50 | REDIS_URL=redis://:password@hostname:port/db_number 51 | ``` 52 | 53 | > Note: for Heroku this variable is created automatically when adding [the appropriate add-on](https://devcenter.heroku.com/articles/heroku-redis). For other hosting companies please consult their documentation. 54 | 55 | Otherwise add your preferred store to the session through the options: 56 | 57 | ```js 58 | const server = require('server'); 59 | // Your own file for the config: 60 | const store = require('./session-store.js'); 61 | server({ session: { store } }, [ 62 | // Routes here 63 | ]); 64 | ``` 65 | 66 | 67 | 68 | ### Alternatives 69 | 70 | Why not just use cookie-session? [Here is an explanation of the alternative](http://stackoverflow.com/a/15745086/938236), but it boils down to: 71 | 72 | - They are more insecure, since all the session data (including sensitive data) is passed forward and backward from the browser to the server in each request. 73 | - If the session data is large then that means adding an unnecessary load to both the server and the browser. 74 | -------------------------------------------------------------------------------- /error/index.test.js: -------------------------------------------------------------------------------- 1 | const ErrorFactory = require('./index.js'); 2 | 3 | describe('server/error', () => { 4 | it('is a function', () => { 5 | expect(ErrorFactory instanceof Function).toBe(true); 6 | }); 7 | 8 | it('canNOT do simple errors', () => { 9 | expect(new ErrorFactory('Hello world').message).not.toBe('Hello world'); 10 | expect(new ErrorFactory('Hello world') instanceof Error).not.toBe(true); 11 | }); 12 | 13 | it('does not create a plain error', () => { 14 | expect(ErrorFactory().message).toBe(undefined); 15 | expect(ErrorFactory() instanceof Function).toBe(true); 16 | expect(ErrorFactory('Hello world').message).toBe(undefined); 17 | expect(ErrorFactory('Hello world') instanceof Function).toBe(true); 18 | expect(ErrorFactory('Hello world', {}).message).toBe(undefined); 19 | expect(ErrorFactory('Hello world', {}) instanceof Function).toBe(true); 20 | }); 21 | 22 | it('can create errors from within the factory', () => { 23 | expect(ErrorFactory('/server/')('test') instanceof Error).toBe(true); 24 | expect(ErrorFactory('/server/')('test').code).toBe('/server/test'); 25 | expect(ErrorFactory('/server/')('test', { status: 500 }).status).toBe(500); 26 | }); 27 | 28 | it('can create errors from within the factory', () => { 29 | expect(ErrorFactory('/server/')('test') instanceof Error).toBe(true); 30 | expect(ErrorFactory('/server/')('test').code).toBe('/server/test'); 31 | expect(ErrorFactory('/server/')('test').namespace).toBe('/server/'); 32 | expect(ErrorFactory('/server/')('test', { status: 500 }).status).toBe(500); 33 | }); 34 | 35 | describe('Namespaces', () => { 36 | const TestError = ErrorFactory('/server/', { status: 500 }); 37 | 38 | it('has the correct defaults', () => { 39 | expect(TestError().status).toBe(500); 40 | expect(TestError().code).toBe('/server'); 41 | expect(TestError().id).toBe('server'); 42 | }); 43 | 44 | it('can extend the errors', () => { 45 | const err = TestError('demo', { status: 501 }); 46 | expect(err.status).toBe(501); 47 | expect(err.code).toBe('/server/demo'); 48 | expect(err.id).toBe('server-demo'); 49 | }); 50 | 51 | it('is the same as with the instance', () => { 52 | const err = TestError('demo', { status: 501 }); 53 | const err2 = new TestError('demo', { status: 501 }); 54 | expect(err).toMatchObject(err2); 55 | }); 56 | }); 57 | 58 | describe('Define errors', () => { 59 | const TestError = ErrorFactory('/server/', { status: 500 }); 60 | TestError.aaa = 'First error'; 61 | 62 | it('has the correct message', () => { 63 | expect(TestError('aaa').message).toBe('First error'); 64 | }); 65 | 66 | it('can define an error with a function', () => { 67 | TestError.bbb = () => `Function error`; 68 | expect(TestError('bbb').message).toBe('Function error'); 69 | }); 70 | 71 | it('defines errors globally', () => { 72 | expect(TestError('bbb').message).toBe('Function error'); 73 | }); 74 | 75 | it('errors are namespaced', () => { 76 | const TestError = ErrorFactory('/server/'); 77 | expect(TestError('bbb').message).toBe(undefined); 78 | }); 79 | 80 | it('gets the options in the interpolation', () => { 81 | TestError.ccc = ({ status }) => `Function error ${status}`; 82 | expect(TestError('ccc', { status: 505 }).message).toBe('Function error 505'); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | // API Specification 2 | // This document is to define server's stable API 3 | // - Main function 4 | // - options 5 | // - middleware 6 | // - context 7 | // - router 8 | // - reply 9 | // - utils 10 | 11 | 12 | 13 | // Main function 14 | const server = require('server'); 15 | 16 | // Definition: an ASYNC function that accepts options and/or middleware 17 | server(opts, mid1, mid2, ...).then(...); 18 | 19 | // Properties (defined below) 20 | server.router; 21 | server.reply; 22 | server.utils; 23 | 24 | 25 | 26 | // Options 27 | const ops = { 28 | 29 | // Simple options 30 | port, 31 | engine, 32 | public, 33 | secret, 34 | log, 35 | 36 | // TODO: MISSING MANY HERE; THIS PART IS NOT YET STABLE 37 | 38 | // Plugins options 39 | core, 40 | parser, 41 | }; 42 | 43 | 44 | 45 | // Middleware 46 | // Definition: (a)sync function, accepts the Context and returns a reply 47 | const mid1 = ctx => { ... }; 48 | const mid2 = async ctx => { ... }; 49 | 50 | // Return types 51 | // String => HTML or PLAIN response 52 | const mid = ctx => 'Hello world'; 53 | // Array/Object => JSON response 54 | const mid = ctx => ['I', 'am', 'json']; 55 | const mid = ctx => ({ hello: 'world' }); // To return an object the extra () is needed 56 | // Reply instance 57 | const mid = ctx => server.reply.send('hello world'); 58 | 59 | 60 | 61 | // Context 62 | // Definition: all of the currently known data. Varies depending on location 63 | // NOTE: there are more properties, but they are not considered stable 64 | ctx.options, // the specified or inherited options 65 | ctx.log, // a method to log things in several levels 66 | ctx.reply, // same as above 67 | ctx.utils, // utilities 68 | ctx.server, // the currently running server instance 69 | 70 | // For middleware/routers 71 | ctx.data, // the parsed body if it's a POST request 72 | ctx.params, // NOTE: NOT YET, rely on ctx.req.params so far 73 | ctx.query, // NOTE: NOT YET, rely on ctx.req.query so far 74 | // ... 75 | 76 | // Non-stable (will change at some point in the future) 77 | ctx.req, // express request; considering removing/changing it in 1.1 78 | ctx.res, // express response; not useful anymore, use server.reply instead 79 | 80 | 81 | 82 | // Router 83 | const router = server.router; 84 | const router = require('server/router'); 85 | 86 | // Definition: handle the different methods and paths requested 87 | router.get('/', mid1); 88 | router.post('/users', mid2); 89 | router.put('/users/:id', mid3); 90 | 91 | // Methods (REST methods not explained): 92 | router.get; 93 | router.post; 94 | router.put; 95 | router.del; 96 | router.socket; // Handle websocket calls 97 | router.error; // Handle errors further up in the chain 98 | 99 | 100 | 101 | // Reply 102 | const reply = server.reply; 103 | const reply = require('server/reply'); 104 | 105 | // Definition: handle the response from your code 106 | // Note: it MUST be returned from the middleware or it won't be executed 107 | reply.cookie; 108 | reply.download; 109 | reply.end; 110 | reply.file; 111 | reply.header; 112 | reply.json; 113 | reply.jsonp; 114 | reply.redirect; 115 | reply.render; 116 | reply.send; 117 | reply.status; 118 | reply.type; 119 | 120 | 121 | 122 | // Utils 123 | const utils = server.utils; 124 | const utils = require('server/utils'); // NOT YET AVAILABLE 125 | 126 | // Definition: some extra utilities to make development easier 127 | utils.modern; // Make express middleware work with server.js 128 | -------------------------------------------------------------------------------- /docs/assets/_toc.scss: -------------------------------------------------------------------------------- 1 | .toc { 2 | background: #fff; 3 | overflow-y: auto; 4 | margin: -3px 0 0 -3px; 5 | max-height: calc(100vh - 50px); 6 | top: 30px; 7 | 8 | .search { 9 | width: calc(100% - 10px); 10 | margin: 5px 0 5px 10px; 11 | border-color: #ccc; 12 | 13 | &.active { 14 | border-color: $primary-color; 15 | } 16 | } 17 | 18 | .searchbox ul { 19 | width: calc(100% - 10px); 20 | margin-left: 10px; 21 | } 22 | 23 | .searchbox .tip { 24 | color: #888; 25 | font-size: .8em; 26 | } 27 | 28 | .searchbox li a { 29 | flex: 0 0 100%; 30 | } 31 | } 32 | 33 | @media all and (min-width: 700px) { 34 | .toc { 35 | padding: 0 10px 0 0; 36 | position: sticky; 37 | } 38 | } 39 | 40 | .toc h2 { 41 | font-size: 1.25em; 42 | margin-left: 0; 43 | padding: 0; 44 | margin-top: 0; 45 | } 46 | 47 | .toc h2 > *, 48 | .toc a { 49 | display: block; 50 | color: inherit; 51 | padding: 0 10px; 52 | overflow: hidden; 53 | white-space: nowrap; 54 | text-overflow: ellipsis; 55 | } 56 | 57 | .toc .label { 58 | float: right; 59 | margin-top: 9px; 60 | margin-right: 2px; 61 | } 62 | 63 | .toc a { 64 | flex: 0 0 calc(100% - 35px); 65 | } 66 | 67 | .toc a.good::after, 68 | .toc a.mid::after, 69 | .toc a.bad::after { 70 | content: ''; 71 | background: $picnic-success; 72 | width: 10px; 73 | height: 10px; 74 | border-radius: 50%; 75 | float: right; 76 | margin: 10px 0 0 0; 77 | opacity: 0.4; 78 | position: absolute; 79 | right: 10px; 80 | } 81 | 82 | @media all and (min-width: 800px) { 83 | .toc a.good::after, 84 | .toc a.mid::after, 85 | .toc a.bad::after { 86 | margin: 12px 0 0 0; 87 | } 88 | } 89 | 90 | .toc li ul a.good::after, 91 | .toc li ul a.mid::after, 92 | .toc li ul a.bad::after { 93 | opacity: 0.2; 94 | } 95 | 96 | .toc a.mid::after { 97 | background: $picnic-warning; 98 | } 99 | 100 | .toc a.bad::after { 101 | background: $primary-color; 102 | } 103 | 104 | .toc a:hover { 105 | color: #0074d9; 106 | background: #eee; 107 | } 108 | 109 | .toc ul { 110 | padding: 0; 111 | margin: 0; 112 | list-style: none; 113 | position: relative; 114 | 115 | &.hidden { 116 | display: none; 117 | } 118 | } 119 | 120 | .toc > ul { 121 | margin-bottom: 50px; 122 | } 123 | 124 | .toc li { 125 | display: flex; 126 | align-items: center; 127 | justify-content: space-between; 128 | line-height: 1.8; 129 | margin: .1em 0; 130 | flex-wrap: wrap; 131 | } 132 | 133 | .toc li li { 134 | margin-left: 45px; 135 | a { 136 | flex: 0 0 100%; 137 | } 138 | } 139 | 140 | .toc .more { 141 | // position: absolute; 142 | // left: 0; 143 | // top: 5px; 144 | flex: 0 0 30px; 145 | display: block; 146 | transition: all .3s ease; 147 | font-size: 30px; 148 | line-height: 24px; 149 | height: 30px; 150 | text-align: center; 151 | width: 30px; 152 | cursor: pointer; 153 | border-radius: 50%; 154 | transform-origin: 50% 50% 0; 155 | 156 | background-image: url('/img/chevron.svg'); 157 | background-size: 18px 18px; 158 | background-repeat: no-repeat; 159 | background-position: 7px 6px; 160 | 161 | -moz-user-select: none; 162 | -webkit-user-select: none; 163 | -ms-user-select: none; 164 | user-select: none; 165 | } 166 | 167 | .toc .more:hover { 168 | color: #0074d9; 169 | background-color: #eee; 170 | } 171 | 172 | .toc .active > .more { 173 | transform: rotateZ(90deg); 174 | } 175 | 176 | .toc .more ~ ul { 177 | flex: 0 0 100%; 178 | max-height: 0; 179 | overflow: hidden; 180 | transition: all .3s ease; 181 | } 182 | 183 | .toc .active > .more ~ ul { 184 | max-height: 1000px; 185 | } 186 | -------------------------------------------------------------------------------- /docs/img/socketio.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | logo full 27 | 28 | 29 | 30 | 50 | 51 | logo full 53 | Created with Sketch. 54 | 56 | 61 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | const filters = require("./docs/filters.js"); 2 | const files = require("./docs/files.js"); 3 | const sass = require("sass"); 4 | 5 | const fs = require("fs"); 6 | 7 | function extract(src) { 8 | const data = {}; 9 | const readme = fs.readFileSync(src + "README.md", "utf-8"); 10 | data.title = (readme.match(/^\#\s(.+)/gm) || []).map(one => 11 | one.replace(/^\#\s/, "") 12 | )[0]; 13 | if (!data.title) 14 | throw new Error("Your file " + file + "/README.md has no h1 in markdown"); 15 | data.sections = (readme.match(/^\#\#[\s](.+)/gm) || []).map(one => 16 | one.replace(/^\#\#\s/, "") 17 | ); 18 | return data; 19 | } 20 | 21 | function getInfo(src) { 22 | delete require.cache[require.resolve(src)]; 23 | const info = {}; 24 | if (/documentation/.test(src)) { 25 | const base = { title: "Introduction", url: "/documentation/" }; 26 | info.introduction = Object.assign({}, extract(src), base); 27 | } 28 | return require(src).reduce((obj, one) => { 29 | return Object.assign({}, obj, { [one]: extract(src + one + "/") }); 30 | }, info); 31 | } 32 | 33 | // Generate the documentation final:origin pairs 34 | const transform = dir => 35 | files(__dirname + "/" + dir) 36 | .filter(str => /\.html\.pug$/.test(str)) 37 | .reduce((docs, one) => { 38 | docs[one.replace(/\.pug$/, "")] = one; 39 | return docs; 40 | }, {}); 41 | 42 | // This builds the library itself 43 | module.exports = function(grunt) { 44 | // Configuration 45 | grunt.initConfig({ 46 | bytesize: { 47 | all: { 48 | src: ["docs/assets/style.min.css", "docs/assets/javascript.js"] 49 | } 50 | }, 51 | 52 | jshint: { 53 | options: { esversion: 6 }, 54 | src: ["Gruntfile.js", "server.js", "src"] 55 | }, 56 | 57 | // Launch a small static server 58 | connect: { 59 | server: { 60 | options: { 61 | port: 3000, 62 | hostname: "*", 63 | base: "docs", 64 | livereload: true, 65 | useAvailablePort: false 66 | } 67 | } 68 | }, 69 | 70 | sass: { 71 | dist: { 72 | options: { implementation: sass, outputStyle: "compressed" }, 73 | files: { "docs/assets/style.min.css": "docs/assets/style.scss" } 74 | } 75 | }, 76 | 77 | pug: { 78 | compile: { 79 | options: { 80 | client: false, 81 | data: file => { 82 | return { 83 | require, 84 | file, 85 | tutorials: getInfo("./docs/tutorials/"), 86 | documentation: getInfo("./docs/documentation/"), 87 | slug: str => str.toLowerCase().replace(/[^\w]+/g, "-") 88 | }; 89 | }, 90 | filters: filters 91 | }, 92 | files: transform("docs") 93 | } 94 | }, 95 | 96 | watch: { 97 | scripts: { 98 | files: [ 99 | "Gruntfile.js", 100 | 101 | // Docs 102 | "docs/**/*.*", 103 | "README.md", 104 | 105 | // For testing: 106 | "server.js", 107 | "src/**/*.js", 108 | 109 | // To bump versions 110 | "package.js" 111 | ], 112 | tasks: ["default"], 113 | options: { 114 | spawn: false, 115 | livereload: true 116 | } 117 | } 118 | } 119 | }); 120 | 121 | grunt.loadNpmTasks("grunt-contrib-connect"); 122 | grunt.loadNpmTasks("grunt-contrib-jshint"); 123 | grunt.loadNpmTasks("grunt-contrib-pug"); 124 | grunt.loadNpmTasks("grunt-contrib-watch"); 125 | grunt.loadNpmTasks("grunt-bytesize"); 126 | grunt.loadNpmTasks("grunt-sass"); 127 | 128 | grunt.registerTask("build", ["sass", "pug"]); 129 | grunt.registerTask("test", ["bytesize"]); 130 | grunt.registerTask("default", ["build", "test", "connect"]); 131 | }; 132 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | These are the changes for server. Follows semver as long as you use the documented features. If you reach into the internals, make sure to lock the version and follow the changes. Feel free to ask any question in Github :) 4 | 5 | 6 | ## 1.0.18 [[reference](https://github.com/franciscop/server/compare/1.0.17...1.0.18)] 7 | 8 | - Fixed tag for npm so it doesn't install alphas by default. 9 | 10 | 11 | ## 1.0.17 [[reference](https://github.com/franciscop/server/compare/1.0.16...1.0.17)] 12 | 13 | - Forgot a couple of debugging console.log(), so had to re-publish without them. 14 | 15 | 16 | 17 | ## 1.0.16 [[reference](https://github.com/franciscop/server/compare/1.0.15...1.0.16)] 18 | 19 | - Add [`options` sub-schema definition in the plugin parser](https://github.com/franciscop/server/issues/60). 20 | 21 | 22 | 23 | ## 1.0.15 [[reference](https://github.com/franciscop/server/compare/1.0.14...1.0.15)] 24 | 25 | - Added [session to the sockets](https://github.com/franciscop/server/issues/55). 26 | - Fixed test and docs for jsonp. 27 | 28 | 29 | 30 | ## 1.0.14 [[reference](https://github.com/franciscop/server/compare/1.0.13...1.0.14)] 31 | 32 | - Added [express session globally](https://github.com/franciscop/server/issues/30) so stores can be created based on that. 33 | 34 | 35 | 36 | ## 1.0.13 [[reference](https://github.com/franciscop/server/compare/1.0.12...1.0.13)] 37 | 38 | - Fixed bug where some error handling might throw an error. From internal testing in one of my projects. 39 | 40 | 41 | 42 | ## 1.0.12 [[reference](https://github.com/franciscop/server/compare/1.0.11...1.0.12)] 43 | 44 | - Remove [the unexpected body that was set by express](https://github.com/franciscop/server/issues/46) when only an status code was sent by making it explicit with status(NUMBER).send(). 45 | 46 | 47 | 48 | ## 1.0.11 [[reference](https://github.com/franciscop/server/compare/1.0.10...1.0.11)] 49 | 50 | - Never published, published on 1.0.12 instead. 51 | 52 | 53 | 54 | ## 1.0.10 [[reference](https://github.com/franciscop/server/compare/1.0.9...1.0.10)] 55 | 56 | - Do not show a warning if [only the status was set but no body](https://github.com/franciscop/server/issues/46) was set. 57 | 58 | 59 | 60 | ## 1.0.9 [[reference](https://github.com/franciscop/server/compare/1.0.8...1.0.9)] 61 | 62 | - Better error handling and warnings when there is no response from the server. Shows error only when it should. 63 | 64 | 65 | 66 | ## 1.0.8 [[reference](https://github.com/franciscop/server/compare/1.0.7...1.0.8)] 67 | 68 | - Never published, published on 1.0.9 instead. 69 | 70 | 71 | 72 | ## 1.0.7 [[reference](https://github.com/franciscop/server/compare/1.0.6...1.0.7)] 73 | 74 | - Fix for [Yarn and npm having different path resolution](https://github.com/franciscop/server/issues/43). This was giving inconsistent results when using yarn (vs the expected one with npm): 75 | 76 | ```js 77 | server( 78 | get('*', (ctx) => status(200)) 79 | ); 80 | ``` 81 | 82 | 83 | 84 | ## 1.0.6 [[reference](https://github.com/franciscop/server/compare/1.0.5...1.0.6)] 85 | 86 | - Never published, published on 1.0.7 instead. 87 | 88 | 89 | 90 | ## 1.0.5 [[reference](https://github.com/franciscop/server/compare/1.0.4...1.0.5)] 91 | 92 | - Fix subdomain order resolution (merged from @nick-woodward). 93 | - Test subdomain handling. 94 | - Removed pointless warning. 95 | 96 | 97 | 98 | ## 1.0.4 [[reference](https://github.com/franciscop/server/compare/1.0.3...1.0.4)] 99 | 100 | - Specify that the `views` for express should be a folder (that can inherit). 101 | - Added environment variable name handling. 102 | 103 | 104 | 105 | ## 1.0.3 [[reference](https://github.com/franciscop/server/compare/1.0.2...1.0.3)] 106 | 107 | - The log plugin is always on since it's needed internally. 108 | 109 | 110 | 111 | ## 1.0.2 112 | 113 | - Better error handling for environment variables. 114 | 115 | 116 | 117 | ## 1.0.1 118 | 119 | - Added the option to disable the CSRF and security plugins independently. 120 | 121 | 122 | 123 | ## 1.0.0 124 | 125 | **First major release.** 126 | -------------------------------------------------------------------------------- /docs/tutorials/getting-started/README.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | In this tutorial you will learn how to get started with Node.js development and create a project from scratch. While there are many ways of doing it, this guide is focused first on making it easy and second on using common tools. 4 | 5 | You will need some basic tools like having git and a code editor ([we recommend Atom](https://atom.io/)) as well as some basic knowledge around your operative system and the terminal/command line. 6 | 7 | 8 | 9 | ## Install Node.js 10 | 11 | This will largely depend on your platform and while you can [just download the binary program from the official page](https://nodejs.org/en/) I would recommend using Node Version Manager for mac or Linux: 12 | 13 | ```bash 14 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash 15 | ``` 16 | 17 | After this, we have to **close and open the terminal** for it to work properly. Then we use NVM to install a current Node.js' version in the terminal: 18 | 19 | ```bash 20 | nvm install node 21 | nvm use node 22 | nvm alias default node 23 | ``` 24 | 25 | > Server requires **Node.js 7.6.0** or newer. **Node.js 8.9.x** LTS is recommended for long-term support from Node.js. 26 | 27 | Great, now we should have Node.js installed and working. To test it's properly installed, execute this on the terminal: 28 | 29 | ```bash 30 | node --version # Should show the version number over 8.x 31 | npm --version # verify this is installed as well over 5.x 32 | ``` 33 | 34 | 35 | 36 | ## Create your project 37 | 38 | Now that we have Node.js installed, let's get started with one project. Create a folder in your computer and then access it from the terminal. To access it use `cd`: 39 | 40 | ```bash 41 | cd "./projects/My Cool Project" 42 | ``` 43 | 44 | From now on, all the operations from the terminal **must** be executed while inside your project in said terminal. 45 | 46 | Then open the project from Atom or your code editor: File > Add project folder... > My Cool Project. 47 | 48 | 49 | 50 | ## Initialize Git and npm 51 | 52 | Git will be used for handling your code, deploying it and working collaboratively. To get your git started execute this in your terminal: 53 | 54 | ```bash 55 | git init 56 | ``` 57 | 58 | It will create a folder called `.git` with all the version history in there. We highly recommend to create a **file** called **`.gitignore`** (yes, a point and the name with no extension) where we add at least the following: 59 | 60 | ``` 61 | *.log 62 | npm-debug.log* 63 | node_modules 64 | .env 65 | ``` 66 | 67 | This is because we want these files and places to only be accessible from our computer, but we don't want to deploy them along the rest of the code. 68 | 69 | Finally we will initialize our project by doing init in the terminal: 70 | 71 | ```bash 72 | npm init 73 | ``` 74 | 75 | It will ask some questions, just answer them or press the "enter" key to accept the default (set the "main" to "index.js"). After answering everything you should have a `package.json` file, so now you can edit the part where it says "scripts" to add this: 76 | 77 | ```json 78 | "scripts": { 79 | "start": "node index.js", 80 | "test": "jest --coverage --forceExit" 81 | }, 82 | ``` 83 | 84 | 85 | 86 | ## Make awesome things! 87 | 88 | That is great! Now you can install the packages that you want like server.js: 89 | 90 | ```bash 91 | npm install server 92 | ``` 93 | 94 | And then create a file called `index.js` with the demo code to see how it works: 95 | 96 | ```js 97 | // Import the library 98 | const server = require('server'); 99 | 100 | // Launch the server to always answer "Hello world" 101 | server(ctx => 'Hello world!'); 102 | ``` 103 | 104 | To execute it **after saving it**, run from the terminal: 105 | 106 | ```bash 107 | npm start 108 | ``` 109 | 110 | And finally open http://localhost:3000/ to see it in action! 111 | 112 | 113 | > Note: this guide was published originally on [Libre University - Getting started](https://en.libre.university/lesson/V1f6Btf8g/Getting%20started) but has since been adapted better only for Node.js. 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **server.js** for Node.js 2 | 3 | [![Downloads](https://img.shields.io/npm/dm/server.svg)](https://npm-stat.com/charts.html?package=server) 4 | [![Status](https://github.com/franciscop/server/workflows/tests/badge.svg)](https://github.com/franciscop/server/actions) 5 | 6 | Powerful server for Node.js that just works so **you can focus on your awesome project**: 7 | 8 | ```js 9 | // Include it and extract some methods for convenience 10 | const server = require('server'); 11 | const { get, post } = server.router; 12 | 13 | // Launch server with options and a couple of routes 14 | server({ port: 8080 }, [ 15 | get('/', ctx => 'Hello world'), 16 | post('/', ctx => { 17 | console.log(ctx.data); 18 | return 'ok'; 19 | }) 20 | ]); 21 | ``` 22 | 23 | ## Getting started 24 | 25 | There's a [whole tutorial on getting started for beginners](https://serverjs.io/tutorials/getting-started/) but the quick version is to first install `server` as a dependency: 26 | 27 | ```bash 28 | npm install server 29 | ``` 30 | 31 | > Server requires **Node.js 7.6.0** or newer. **Node.js 8.x.y** LTS is recommended. 32 | 33 | Then you can create a file called `index.js` with this code: 34 | 35 | ```js 36 | // Include the server in your file 37 | const server = require('server'); 38 | const { get, post } = server.router; 39 | 40 | // Handle requests to the url "/" ( http://localhost:3000/ ) 41 | server([ 42 | get('/', ctx => 'Hello world!') 43 | ]); 44 | ``` 45 | 46 | Execute this in the terminal to get the server started: 47 | 48 | ```bash 49 | node . 50 | ``` 51 | 52 | And finally, open your browser on [localhost:3000](http://localhost:3000/) and you should see 'Hello world!' on your browser. 53 | 54 | 55 | 56 | ## Documentation 57 | 58 | The library is documented here: 59 | 60 | Full Documentation 61 | 62 | You can also download the repository and try the examples by browsing to them and `node .` inside each of them in `/examples`. 63 | 64 | 65 | 66 | ## Use cases 67 | 68 | The package `server` is great for many situations. Let's see some of them: 69 | 70 | 71 | ### Small to medium projects 72 | 73 | Everything works out of the box, you get great support for most features and you can easily tap into Express' middleware ecosystem. What's not to love? 74 | 75 | Some of the included features: body and file parsers, cookies, sessions, websockets, Redis, gzip, favicon, csrf, SSL, etc. They just work so you will save a headache or two and can focus on your actual project. Get a simple form going: 76 | 77 | ```js 78 | const server = require('server'); 79 | const { get, post } = server.router; 80 | const { render, redirect } = server.reply; 81 | 82 | server( 83 | get('/', () => render('index.pug')), 84 | post('/', ctx => { 85 | console.log(ctx.data); 86 | return redirect('/'); 87 | }) 88 | ); 89 | ``` 90 | 91 | 92 | 93 | ### API design 94 | 95 | From the flexibility and expressivity of the bundle, designing APIs is a breeze: 96 | 97 | ```js 98 | // books/router.js 99 | const { get, post, put, del } = require('server/router'); 100 | const ctrl = require('./controller'); 101 | 102 | module.exports = [ 103 | get('/book', ctrl.list), 104 | get('/book/:id', ctrl.item), 105 | post('/book', ctrl.create), 106 | put('/book/:id', ctrl.update), 107 | del('/book/:id', ctrl.delete) 108 | ]; 109 | ``` 110 | 111 | 112 | 113 | ### Real time 114 | 115 | Websockets were never this easy to use! With socket.io on the front-end, you can simply do this in the back-end to handle those events: 116 | 117 | ```js 118 | // chat/router.js 119 | const { socket } = require('server/router'); 120 | const ctrl = require('./controller'); 121 | 122 | module.exports = [ 123 | socket('connect', ctrl.join), 124 | socket('message', ctrl.message), 125 | socket('disconnect', ctrl.leave) 126 | ]; 127 | ``` 128 | 129 | 130 | 131 | ## Author & support 132 | 133 | This package was created by [Francisco Presencia](http://francisco.io/) but hopefully developed and maintained by many others. See the [the list of contributors here](https://github.com/franciscop/server/graphs/contributors). 134 | 135 | You can also [sponsor the project](https://serverjs.io/sponsor), get your logo in here and some other perks with tons of ♥ 136 | -------------------------------------------------------------------------------- /plugins/parser/integration.test.js: -------------------------------------------------------------------------------- 1 | // External libraries used 2 | const { cookie } = require('server/reply'); 3 | const run = require('server/test/run'); 4 | const fs = require('fs'); 5 | run.options = { security: false }; 6 | 7 | // Local helpers and data 8 | const logo = fs.createReadStream(__dirname + '/../../test/logo.png'); 9 | const content = ctx => ctx.headers['content-type']; 10 | 11 | 12 | describe('Default modules', () => { 13 | 14 | it('bodyParser', async () => { 15 | const mid = ctx => { 16 | expect(ctx.data).toEqual(ctx.req.body); 17 | expect(ctx.data).toBeDefined(); 18 | expect(ctx.data.hello).toBe('世界'); 19 | expect(content(ctx)).toBe('application/x-www-form-urlencoded'); 20 | return 'Hello 世界'; 21 | }; 22 | 23 | const res = await run(mid).post('/', { form: 'hello=世界' }); 24 | expect(res.body).toBe('Hello 世界'); 25 | }); 26 | 27 | it('dataParser', async () => { 28 | const mid = ctx => ctx.files.logo; 29 | const res = await run(mid).post('/', { formData: { logo } }); 30 | 31 | expect(res.body.name).toBe('logo.png'); 32 | expect(res.body.type).toBe('image/png'); 33 | expect(res.body.size).toBe(30587); 34 | }); 35 | 36 | // It can *set* cookies from the server() 37 | // TODO: it can *get* cookies from the server() 38 | it('cookieParser', async () => { 39 | const mid = () => cookie('place', '世界').send('Hello 世界'); 40 | 41 | const res = await run(mid).post('/', { body: { place: '世界' } }); 42 | const cookies = res.headers['set-cookie'].join(); 43 | expect(cookies).toMatch('place=%E4%B8%96%E7%95%8C'); 44 | }); 45 | 46 | // Change the method to the specified one 47 | it('method-override through header', async () => { 48 | const mid = ctx => { 49 | expect(ctx.method).toBe('PUT'); 50 | expect(ctx.originalMethod).toBe('POST'); 51 | return 'Hello 世界'; 52 | }; 53 | 54 | const headers = { 'X-HTTP-Method-Override': 'PUT' }; 55 | const res = await run(mid).post('/', { headers }); 56 | expect(res.body).toBe('Hello 世界'); 57 | }); 58 | 59 | // Can overwrite the IP 60 | it.only('method-override through header', async () => { 61 | const mid = ctx => { 62 | expect(ctx.ip).toBe('123.123.123.123'); 63 | return 'Hello 世界'; 64 | }; 65 | 66 | const headers = { 'X-Forwarded-For': '123.123.123.123, 111.111.111.111' }; 67 | const res = await run(mid).post('/', { headers }); 68 | expect(res.body).toBe('Hello 世界'); 69 | }); 70 | 71 | // Change the method to the specified one 72 | it('override-method works with a string', async () => { 73 | const mid = ctx => { 74 | expect(ctx.method).toBe('PUT'); 75 | expect(ctx.originalMethod).toBe('POST'); 76 | return 'Hello 世界'; 77 | }; 78 | 79 | const headers = { 'X-HTTP-Method-Override': 'PUT' }; 80 | const res = await run({ parser: { 81 | method: 'X-HTTP-Method-Override' 82 | } }, mid).post('/', { headers }); 83 | expect(res.body).toBe('Hello 世界'); 84 | }); 85 | 86 | // Change the method to the specified one 87 | it('override-method works with an array', async () => { 88 | const mid = ctx => { 89 | expect(ctx.method).toBe('PUT'); 90 | expect(ctx.originalMethod).toBe('POST'); 91 | return 'Hello 世界'; 92 | }; 93 | 94 | const headers = { 'X-HTTP-Method-Override': 'PUT' }; 95 | const res = await run({ parser: { 96 | method: ['X-HTTP-Method-Override'] 97 | } }, mid).post('/', { headers }); 98 | expect(res.body).toBe('Hello 世界'); 99 | }); 100 | 101 | // TODO: check more options 102 | }); 103 | 104 | 105 | 106 | describe('Cancel parts through options', () => { 107 | 108 | it('can cancel bodyParser', async () => { 109 | const options = { parser: { body: false } }; 110 | const mid = ctx => { 111 | expect(ctx.body).toEqual({}); 112 | expect(ctx.headers['content-type']).toBe('application/x-www-form-urlencoded'); 113 | return 'Hello 世界'; 114 | }; 115 | 116 | const res = await run(options, mid).post('/', { form: 'hello=世界' }); 117 | expect(res.body).toBe('Hello 世界'); 118 | }); 119 | 120 | it('can cancel jsonParser', async () => { 121 | const mid = ctx => { 122 | expect(ctx.data).toEqual(ctx.req.body); 123 | expect(ctx.data).toEqual({}); 124 | expect(content(ctx)).toBe('application/json'); 125 | return 'Hello 世界'; 126 | }; 127 | 128 | const res = await run({ parser: { json: false }}, mid).post('/', { body: { hello: '世界' }}); 129 | expect(res.body).toBe('Hello 世界'); 130 | }); 131 | 132 | // TODO: check all others can be cancelled 133 | }); 134 | -------------------------------------------------------------------------------- /docs/img/battery.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 41 | 43 | 44 | 46 | image/svg+xml 47 | 49 | 50 | 51 | 52 | 53 | 58 | 65 | 71 | 77 | 82 | 87 | 92 | 99 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /reply/reply.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('mz/fs'); 3 | 4 | 5 | const Reply = function (name, ...args) { 6 | this.stack = []; 7 | this[name](...args); 8 | return this; 9 | }; 10 | 11 | 12 | 13 | Reply.prototype.cookie = function (...args) { 14 | this.stack.push(ctx => { 15 | ctx.res.cookie(...args); 16 | }); 17 | return this; 18 | }; 19 | 20 | 21 | 22 | Reply.prototype.download = function (...args) { 23 | 24 | // Guard clauses 25 | if (args.length < 1) { 26 | throw new Error('download() expects a path as the first argument'); 27 | } 28 | 29 | if (args.length < 2) { 30 | throw new Error('download() expects a filename as the second argument'); 31 | } 32 | 33 | if (args.length > 2) { 34 | throw new Error('download() only expects two arguments, path and filename. The rest of them will be ignored'); 35 | } 36 | 37 | let [file, opts] = args; 38 | if (!path.isAbsolute(file)) { 39 | file = path.resolve(process.cwd(), file); 40 | } 41 | 42 | this.stack.push(async ctx => { 43 | if (!await fs.exists(file)) { 44 | throw new Error(`The file "${file}" does not exist. Make sure that you set an absolute path or a relative path to the root of your project`); 45 | } 46 | return new Promise((resolve, reject) => { 47 | ctx.res.download(file, opts, err => err ? reject(err) : resolve()); 48 | }); 49 | }); 50 | 51 | return this; 52 | }; 53 | 54 | 55 | 56 | Reply.prototype.end = function () { 57 | this.stack.push(ctx => { 58 | ctx.res.end(); 59 | }); 60 | return this; 61 | }; 62 | 63 | 64 | 65 | Reply.prototype.file = function (...args) { 66 | 67 | // Guard clauses 68 | if (args.length < 1) { 69 | throw new Error('file() expects a path as the first argument'); 70 | } 71 | 72 | if (args.length > 2) { 73 | throw new Error(`file() only expects two arguments, the path and options, but ${args.length} were provided.`); 74 | } 75 | 76 | let [file, opts = {}] = args; 77 | if (!path.isAbsolute(file)) { 78 | file = path.resolve(process.cwd(), file); 79 | } 80 | 81 | this.stack.push(async ctx => { 82 | if (!await fs.exists(file)) { 83 | throw new Error(`The file "${file}" does not exist. Make sure that you set an absolute path or a relative path to the root of your project`); 84 | } 85 | return new Promise((resolve, reject) => { 86 | ctx.res.sendFile(file, opts, err => err ? reject(err) : resolve()); 87 | }); 88 | }); 89 | 90 | return this; 91 | }; 92 | 93 | 94 | 95 | Reply.prototype.header = function (...args) { 96 | this.stack.push(ctx => { 97 | ctx.res.header(...args); 98 | }); 99 | return this; 100 | }; 101 | 102 | Reply.prototype.json = function (...args) { 103 | this.stack.push(ctx => { 104 | ctx.res.json(...args); 105 | }); 106 | return this; 107 | }; 108 | 109 | Reply.prototype.jsonp = function (...args) { 110 | this.stack.push(ctx => { 111 | ctx.res.jsonp(...args); 112 | }); 113 | return this; 114 | }; 115 | 116 | Reply.prototype.redirect = function (...args) { 117 | this.stack.push(ctx => { 118 | ctx.res.redirect(...args); 119 | }); 120 | return this; 121 | }; 122 | 123 | Reply.prototype.render = function (...args) { 124 | 125 | // Guard clauses 126 | if (args.length < 1) { 127 | throw new Error('file() expects a path'); 128 | } 129 | 130 | if (args.length > 2) { 131 | throw new Error('file() expects a path and options but nothing else'); 132 | } 133 | 134 | let [file, opts = {}] = args; 135 | 136 | this.stack.push(ctx => new Promise((resolve, reject) => { 137 | // Note: if callback is provided, it does not send() automatically 138 | const cb = (err, html) => err ? reject(err) : resolve(ctx.res.send(html)); 139 | ctx.res.render(file, opts, cb); 140 | })); 141 | return this; 142 | }; 143 | 144 | Reply.prototype.send = function (...args) { 145 | 146 | // If we are trying to send the context 147 | if (args[0] && args[0].close && args[0].close instanceof Function) { 148 | throw new Error('Never send the context, request or response as those are a security risk'); 149 | } 150 | 151 | this.stack.push(ctx => { 152 | ctx.res.send(...args); 153 | }); 154 | return this; 155 | }; 156 | 157 | Reply.prototype.status = function (...args) { 158 | this.stack.push(ctx => { 159 | // In case there is no response, it'll respond with the status 160 | ctx.res.explicitStatus = true; 161 | ctx.res.status(...args); 162 | }); 163 | return this; 164 | }; 165 | 166 | Reply.prototype.type = function (...args) { 167 | this.stack.push(ctx => { 168 | ctx.res.type(...args); 169 | }); 170 | return this; 171 | }; 172 | 173 | Reply.prototype.exec = async function (ctx) { 174 | for (let cb of this.stack) { 175 | await cb(ctx); 176 | } 177 | this.stack = []; 178 | }; 179 | 180 | // This will make that the first time a function is called it starts a new stack 181 | module.exports = Reply; 182 | -------------------------------------------------------------------------------- /test/run.js: -------------------------------------------------------------------------------- 1 | const server = require('../'); 2 | const request = require('request-promises'); 3 | const port = require('./port'); 4 | 5 | // Make an object with the options as expected by request() 6 | const normalize = (method, url, port, options) => { 7 | // Make sure it's a simple object 8 | if (typeof options === 'string') options = { url: options }; 9 | 10 | // Assign independent parts 11 | options = Object.assign({}, options, { url, method }); 12 | 13 | // Make sure it has a right URL or localhost otherwise 14 | if (!/^https?:\/\//.test(options.url)) { 15 | options.url = `http://localhost:${port}${options.url}`; 16 | } 17 | 18 | // Set it to send a JSON when appropriate 19 | if (options.body && typeof options.body === 'object') { 20 | options.json = true; 21 | } 22 | 23 | // Finally return the fully formed object 24 | return options; 25 | }; 26 | 27 | // Parse the server options 28 | const serverOptions = async middle => { 29 | // First parameter can be: 30 | // - options: Number || Object (cannot be ID'd) 31 | // - middleware: undefined || null || Boolean || Function || Array 32 | let opts = 33 | typeof middle[0] === 'undefined' || 34 | typeof middle[0] === 'boolean' || 35 | typeof middle[0] === 'string' || 36 | middle[0] === null || 37 | middle[0] instanceof Function || 38 | middle[0] instanceof Array 39 | ? {} 40 | : middle.shift(); 41 | 42 | // In case the port is the defaults one 43 | let synthetic = !opts || !opts.port; 44 | await opts; 45 | 46 | // Create the port when none was specified 47 | if (synthetic) opts.port = port(); 48 | 49 | // Be able to set global variables from outside 50 | opts = Object.assign({}, opts, module.exports.options || {}, { 51 | env: undefined, 52 | secret: undefined 53 | }); 54 | 55 | return opts; 56 | }; 57 | 58 | module.exports = function(...middle) { 59 | // Make sure we are working with an instance 60 | if (!(this instanceof module.exports)) { 61 | return new module.exports(...middle); 62 | } 63 | 64 | const launch = async (method, url, reqOpts) => { 65 | // Parse the server options 66 | const opts = await serverOptions(middle); 67 | 68 | const error = server.router.error(ctx => { 69 | if (!ctx.res.headersSent) { 70 | return server.reply.status(500).send(ctx.error.message); 71 | } 72 | }); 73 | 74 | const ctx = await server(opts, middle, opts.raw ? false : error); 75 | 76 | ctx.close = () => 77 | new Promise((resolve, reject) => { 78 | ctx.server.close(err => (err ? reject(err) : resolve())); 79 | }); 80 | if (!method) return ctx; 81 | const res = await request( 82 | normalize(method, url, ctx.options.port, reqOpts) 83 | ); 84 | // Fix small bug. TODO: report it 85 | res.method = res.request.method; 86 | res.status = res.statusCode; 87 | res.options = ctx.options; 88 | if ( 89 | /application\/json/.test(res.headers['content-type']) && 90 | typeof res.body === 'string' 91 | ) { 92 | res.rawBody = res.body; 93 | res.body = JSON.parse(res.body); 94 | } 95 | res.ctx = ctx; 96 | 97 | // Close the server once it has all finished 98 | await ctx.close(); 99 | 100 | // Return the response that happened from the server 101 | return res; 102 | }; 103 | 104 | this.alive = async cb => { 105 | let instance; 106 | try { 107 | instance = await launch(); 108 | const port = instance.options.port; 109 | const requestApi = request.defaults({ jar: request.jar() }); 110 | const generic = method => async (url, options) => { 111 | const res = await requestApi(normalize(method, url, port, options)); 112 | res.method = res.request.method; 113 | res.status = res.statusCode; 114 | if ( 115 | /application\/json/.test(res.headers['content-type']) && 116 | typeof res.body === 'string' 117 | ) { 118 | res.rawBody = res.body; 119 | res.body = JSON.parse(res.body); 120 | } 121 | // console.log(instance); 122 | res.ctx = instance; 123 | return res; 124 | }; 125 | const api = { 126 | get: generic('GET'), 127 | head: generic('HEAD'), 128 | post: generic('POST'), 129 | put: generic('PUT'), 130 | del: generic('DELETE'), 131 | ctx: instance 132 | }; 133 | await cb(api); 134 | } catch (err) { 135 | if (!instance) { 136 | console.log(err); 137 | } 138 | throw err; 139 | } finally { 140 | instance.close(); 141 | } 142 | }; 143 | this.get = (url, options) => launch('GET', url, options); 144 | this.head = (url, options) => launch('HEAD', url, options); 145 | this.post = (url, options) => launch('POST', url, options); 146 | this.put = (url, options) => launch('PUT', url, options); 147 | this.del = (url, options) => launch('DELETE', url, options); 148 | return this; 149 | }; 150 | 151 | module.exports.options = {}; 152 | -------------------------------------------------------------------------------- /src/modern/modern.test.js: -------------------------------------------------------------------------------- 1 | const join = require('../join'); 2 | const modern = require('./index'); 3 | const middle = (req, res, next) => next(); 4 | const ctx = { req: {}, res: {} }; 5 | 6 | describe('initializes', () => { 7 | it('returns a function', () => { 8 | expect(typeof modern(middle)).toBe('function'); 9 | }); 10 | 11 | it('the returned modern middleware has 1 arg', () => { 12 | expect(modern(middle).length).toBe(1); 13 | }); 14 | 15 | it('requires an argument', () => { 16 | expect(() => modern()).toThrow(); 17 | }); 18 | 19 | it('a non-function argument throws', () => { 20 | expect(() => modern(true)).toThrow(); 21 | expect(() => modern(5)).toThrow(); 22 | expect(() => modern('')).toThrow(); 23 | expect(() => modern([])).toThrow(); 24 | expect(() => modern({})).toThrow(); 25 | 26 | expect(() => modern(() => {})).not.toThrow(); 27 | }); 28 | }); 29 | 30 | 31 | 32 | describe('call the middleware', () => { 33 | it('returns a promise when called', () => { 34 | expect(modern(middle)(ctx) instanceof Promise).toBe(true); 35 | }); 36 | 37 | it('requires the context to be called', async () => { 38 | expect(modern(middle)()).rejects.toBeDefined(); 39 | }); 40 | 41 | it('rejected with empty context', async () => { 42 | expect(modern(middle)({})).rejects.toBeDefined(); 43 | }); 44 | 45 | it('rejected without res', async () => { 46 | expect(modern(middle)({ req: {} })).rejects.toBeDefined(); 47 | }); 48 | 49 | it('rejected without req', async () => { 50 | expect(modern(middle)({ res: {} })).rejects.toBeDefined(); 51 | }); 52 | }); 53 | 54 | 55 | 56 | describe('Middleware handles the promise', () => { 57 | it('resolves when next is called empty', async () => { 58 | await modern((req, res, next) => next())(ctx); 59 | }); 60 | 61 | it('cannot handle error middleware', async () => { 62 | // eslint-disable-next-line no-unused-vars 63 | expect(() => modern((err, req, res, next) => {})).toThrow(); 64 | }); 65 | 66 | it('keeps the context', async () => { 67 | const ctx = { req: 1, res: 2 }; 68 | await modern((req, res, next) => next())(ctx); 69 | expect(ctx.req).toBe(1); 70 | expect(ctx.res).toBe(2); 71 | }); 72 | 73 | it('can modify the context', async () => { 74 | const middle = (req, res, next) => { 75 | req.user = 'myname'; 76 | res.send = 'sending'; 77 | next(); 78 | }; 79 | const ctx = { req: {}, res: {} }; 80 | await modern(middle)(ctx); 81 | expect(ctx.req.user).toBe('myname'); 82 | expect(ctx.res.send).toBe('sending'); 83 | }); 84 | 85 | it('has chainable context', async () => { 86 | const ctx = { req: { user: 'a' }, res: { send: 'b' } }; 87 | const middle = (req, res, next) => { 88 | req.user += 1; 89 | res.send += 2; 90 | next(); 91 | }; 92 | await modern(middle)(ctx).then(() => modern(middle)(ctx)); 93 | expect(ctx.req.user).toBe('a11'); 94 | expect(ctx.res.send).toBe('b22'); 95 | }); 96 | 97 | it('factory can receive options', async () => { 98 | 99 | // The full context 100 | const ctx = { 101 | req: { user: 'a' }, 102 | res: { send: 'b' }, 103 | options: { extra: 1} 104 | }; 105 | 106 | // A middleware factory 107 | const factory = opts => { 108 | return (req, res, next) => { 109 | req.user += opts.extra; 110 | res.send += opts.extra; 111 | next(); 112 | }; 113 | }; 114 | 115 | // Plain ol' middleware 116 | const factored = factory({ extra: 1 }); 117 | 118 | // We need to pass it and then re-call it 119 | const middles = [ 120 | 121 | // Native sync, this could be extracted to '({ req, res, options })' 122 | ctx => { 123 | ctx.req.user += ctx.options.extra; 124 | ctx.res.send += ctx.options.extra; 125 | }, 126 | 127 | // Native async 128 | ctx => new Promise((resolve) => { 129 | ctx.req.user += ctx.options.extra; 130 | ctx.res.send += ctx.options.extra; 131 | resolve(); 132 | }), 133 | 134 | // Hardcoded case: 135 | modern((req, res, next) => { 136 | req.user += 1; 137 | res.send += 1; 138 | next(); 139 | }), 140 | 141 | // Using some info from the context: 142 | ctx => modern((req, res, next) => { 143 | req.user += ctx.options.extra; 144 | res.send += ctx.options.extra; 145 | next(); 146 | })(ctx), 147 | 148 | // The definition might come from a factory 149 | ctx => modern(factory({ extra: ctx.options.extra }))(ctx), 150 | 151 | // The same as above but already defined 152 | ctx => modern(factored)(ctx) 153 | ]; 154 | 155 | await join(middles)(ctx); 156 | expect(ctx.req.user).toBe('a111111'); 157 | expect(ctx.res.send).toBe('b111111'); 158 | }); 159 | 160 | it('rejects when next is called with an error', async () => { 161 | const wrong = (req, res, next) => next(new Error('Custom error')); 162 | expect(modern(wrong)(ctx)).rejects.toBeDefined(); 163 | }); 164 | 165 | it('does not resolve nor reject if next is not called', async () => { 166 | modern(() => {})(ctx).then(() => { 167 | expect('It was resolved').toBe(false); 168 | }).catch(() => { 169 | expect('It was rejected').toBe(false); 170 | }); 171 | return new Promise(resolve => { 172 | setTimeout(() => resolve(), 1000); 173 | }); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /docs/documentation/errors/README.md: -------------------------------------------------------------------------------- 1 | # Errors 2 | 3 |
4 | If you happen to stumble here, this bit of the documentation is outdated and follows some old code. Please help us improve the project and the docs so we can make it into the official release. 5 |
6 | 7 | There are many type of errors that can occur with server.js and here we try to explain them and how to fix them. They are divided by category: where/why they are originated. 8 | 9 | We also overview here how to handle errors. You have to [first define it](#define-an-error), then [throw the error](#throw-the-error) and finally [handle the error](#error-handling). 10 | 11 | 12 | ### Define an error 13 | 14 | To define an error in your code the best way to do it is to use the package `human-error` (by the author of server), since it's made to combine perfectly with server.js. In the future we might integrate it, but so far they are kept separated. 15 | 16 | To define an error, create a different file that will contain all or part of your errors, here called `errors.js` for our site `mycat.com`: 17 | 18 | ```js 19 | // errors.js 20 | const errors = require('human-error')(); // <-- notice this 21 | 22 | errors['/mycat/nogithubsecret'] = ` 23 | There is no github secret set up. Make sure you have saved it in your '.env', 24 | and if you don't have access go see Tom and he'll explain what to do next. 25 | https://mycat.com/guide/setup/#github 26 | `; 27 | 28 | module.exports = errors; 29 | ``` 30 | 31 | ### Throw the error 32 | 33 | Now let's use it, to do so we'll just need to import this file and throw the corresponding error: 34 | 35 | ```js 36 | const server = require('server'); 37 | const HumanError = require('./errors'); 38 | 39 | server(ctx => { 40 | if (!ctx.options.githubsecret) { 41 | throw new HumanError('/mycat/nogithubsecret'); 42 | } 43 | }); 44 | ``` 45 | 46 | Try it! Run the code with `node .` and try accessing [http://localhost:3000/](http://localhost:3000). You should see a `server error` on the front-end and the proper description in the back-end. 47 | 48 | 49 | 50 | ### Error handling 51 | 52 | Now this was an error for the developers where we want to be explicit and show the error clearly. For users thought things change a bit and are greatly improved by server's error handling. 53 | 54 | First let's deal with super type checking: 55 | 56 | ```js 57 | const route = get('/post/:id', ctx => { 58 | if (!/^\d+$/.test(ctx.params.id)) { 59 | throw new HumanError('/mycat/type/invalid', { base: '/post' }); 60 | } 61 | }); 62 | 63 | // Handle a wrong id error and redirect to a 404 64 | const handle = error('/mycat/type/invalid', async ctx => { 65 | return redirect(`/${ctx.error.base || ''}?message=notfound`); 66 | }); 67 | 68 | // Handle all type errors in the namespace "mycat" 69 | const handleType = error('/mycat/type', () => { 70 | return redirect(`/${ctx.error.base || ''}?message=notfound`); 71 | }); 72 | 73 | // Handle all kind of unhandled errors in the namespace "mycat" 74 | const handleAll = error('/mycat', () => { 75 | return status(500); 76 | }); 77 | ``` 78 | 79 | 80 | Let's say that someone is trying to access something they don't have access to. Like deleting a comment that is not theirs: 81 | 82 | ```js 83 | // comments.js 84 | module.exports = [ 85 | ... 86 | del('/comment/:id', async ctx => { 87 | const comment = await db.comment.findOne({ _id: ctx.params.id }); 88 | if (!comment.author.equals(ctx.user._id)) { 89 | throw new HumanError('/mycat/auth/unauthorized', { user: ctx.user._id }); 90 | } 91 | }) 92 | ]; 93 | ``` 94 | 95 | Later on you can handle this specific error, we could log these specific kind of errors, etc. 96 | 97 | 98 | 99 | 100 | ## Native 101 | 102 | ### /server/native/portused 103 | 104 | This happens when you try to launch `server` in a port that is already being used by another process. It can be another server process or a totally independent process. To fix it you can do: 105 | 106 | - Check that there are no other terminals running this process already. 107 | - Change the port for the server such as `server({ port: 5000 });`. 108 | - Find out what process is already using the port and stop it. In Linux: `fuser -k -n tcp 3000`. 109 | 110 | Example on when this error is happening: 111 | 112 | ```js 113 | const server = require('server'); 114 | // DO NOT DO THIS: 115 | server(3000); 116 | server(3000); 117 | ``` 118 | 119 | To fix it, invoke it with a different port: 120 | 121 | ```js 122 | const server = require('server'); 123 | server(2000); 124 | server(3000); 125 | ``` 126 | 127 | 128 | 129 | ## Options 130 | 131 | These errors are related to server's options. 132 | 133 | ### /server/options/portnotanumber 134 | 135 | 136 | 137 | 138 | ## Core 139 | 140 | These errors occur when handling a specific part of server.js. 141 | 142 | ### /server/core/missingmiddleware 143 | 144 | This will normally happen if you are trying to create a `server` middleware from an `express` middleware but forget to actually pass express' middleware. 145 | 146 | This error happens when you call `modern()` with an empty or falsy value: 147 | 148 | ```js 149 | const { modern } = server.utils; 150 | const middle = modern(); // Error 151 | ``` 152 | 153 | 154 | 155 | ### /server/core/invalidmiddleware 156 | 157 | This happens when you try to call `modern()` with an argument that is not an old-style middleware. The first and only argument for `modern()` is a function with `express`' middleware signature. 158 | 159 | This error should also tell you dynamically which type of argument you passed. 160 | 161 | ```js 162 | const { modern } = server.utils; 163 | const middle = modern('hello'); 164 | ``` 165 | --------------------------------------------------------------------------------