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 |
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 |
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 |
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 |
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 touchPaypal 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 |
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 |
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 | [](https://npm-stat.com/charts.html?package=server)
4 | [](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 |
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 |
--------------------------------------------------------------------------------