New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

lark-router

Package Overview
Dependencies
Maintainers
3
Versions
21
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

lark-router - npm Package Compare versions

Comparing version 1.0.2 to 1.1.0

examples/async.js

383

index.js
/**
* Lark router, auto generate routes by directory structure
* Lark Router
**/
'use strict';
import _debug from 'debug';
import chalk from 'chalk';
import extend from 'extend';
import path from 'path';
import fs from 'fs';
import KoaRouter from 'koa-router';
import escapeRegexp from 'escape-string-regexp';
const $ = require('lodash');
const debug = require('debug')('lark-router.Router');
const assert = require('assert');
const extend = require('extend');
const fs = require('fs');
const methods = require('methods');
const path = require('path');
const path2regexp = require('path-to-regexp');
const Switcher = require('switch-case');
const EventEmitter = require('events').EventEmitter;
const debug = _debug('lark-router');
debug('loading ...');
class Router extends EventEmitter {
static get defaultConfig () {
return extend(true, {}, defaultConfig);
}
/**
* Extends KoaRouter with the following methods:
* @method create(options) returns a new instance of Router
* @method load(directory, prefix) generate routes by the directory structure
**/
class Router extends KoaRouter {
// @overwrite
constructor (options = {}) {
if (options && !(options instanceof Object)) {
throw new Error('Options must be an object if given');
}
debug('Router: Router.constructor');
super(options);
debug('constructing ...');
super();
this.opts.param_prefix = this.opts.param_prefix || '_';
if ('string' !== typeof this.opts.param_prefix || !this.opts.param_prefix.match(/^\S+$/)) {
throw new Error("Router options param_prefix must be a string matching patter \\S+");
}
this.opts.prefix_esc = escapeRegexp(this.opts.param_prefix);
this.adapter = {
parseFileName: defaultParseFileName,
};
this.opts.default = this.opts.default || 'index.js';
if ('string' !== typeof this.opts.default || this.opts.default.length === 0) {
throw new Error("Router options default must be a string");
}
this._switcher = new Switcher();
this._enabledMethods = [];
this.configure(options);
// overwrting switcher methods
this._switcher.prepare = this._prepare.bind(this);
this._switcher.match = this._match.bind(this);
this._switcher.nesting = this._nesting.bind(this);
this._switcher.execute = this._execute.bind(this);
}
static create (options) {
debug('Router: Router.create');
return new Router(options);
// @overwrite switcher
_prepare (o, req, ...args) {
debug('preparing ...');
o = extend(true, {}, o);
return [o, req, ...args];
}
load (root, prefix) {
debug('Router: loading by root path ' + root);
if ('string' !== typeof root) {
throw new Error('Router loading root path is not a string');
// @overwrite switcher
_match (condition, o, req, ...args) {
debug('matching ...');
assert('string' === typeof o.method && 'string' === typeof o.path, 'Method and URL must be string!');
debug('testing [' + o.method.toUpperCase() + ' ' + o.path + '] with [' + condition.method.toUpperCase() + ' ' + condition.pathexp + '] ...');
if ((condition.method !== o.method || (this._config.max > 0 && this._config.max <= req.routed))
&& !this._specialMethods.includes(condition.method)) {
return false;
}
root = path.normalize(root);
const result = condition.regexp.exec(o.path);
if (!result) return false;
if (!path.isAbsolute(root)) {
debug('Router: root is not absolute, make an absolute one');
root = path.join(path.dirname(process.mainModule.filename), root);
if ((condition.method === 'routed' && !req.routed) ||
(condition.method === 'other' && req.routed)) {
return false;
}
if (prefix) {
prefix = path.normalize(prefix);
if (!prefix || !prefix[0] || prefix[0] === '.') {
throw new Error('Invalid router prefix ' + prefix);
}
debug('Router: create a new Router to load with prefix ' + prefix);
const opts = extend(true, {}, this.opts);
opts.routePrefix = opts.routePrefix || '';
opts.routePrefix += prefix;
const router = Router.create(opts).load(root);
debug('Router: using the router with prefix ' + prefix);
this.use(prefix, router.routes());
return this;
}
assert(result.length >= 1, 'Internal Error!');
debug('Router; loading by directory structure of ' + root);
debug('matched!');
req.routed++;
/**
* First load all files, then load directories recrusively
**/
const dirlist = [];
const filelist = [];
const list = fs.readdirSync(root);
for (const filename of list) {
let routePath = name2routePath(filename, this.options);
if (routePath === false) {
continue;
const keys = condition.regexp.keys;
const startIndex = Object.keys(o.params).length;
for (let i = 1; i < result.length; i++) {
const index = i - 1;
let name = keys[index].name;
if ('number' === typeof name) {
name += startIndex;
}
routePath = '/' + routePath;
const item = { filename, routePath };
const absolutePath = path.join(root, filename);
const stat = fs.statSync(absolutePath);
if (stat.isDirectory()) {
dirlist.push(item);
}
else if (stat.isFile()) {
filelist.push(item);
}
assert(!o.params.hasOwnProperty(name), "Duplicated path param name [" + name + "]!");
o.params[name] = $.cloneDeep(result[i]) || '/';
}
for (const item of filelist) {
loadRouteByFilename(this, item.filename, item.routePath, root);
}
for (const item of dirlist) {
this.load(path.join(root, item.filename), item.routePath);
}
return this;
condition.nesting && (o.subroutine = this.subroutine);
return true;
}
}
// @overwrite switcher
_nesting (o, req, ...args) {
debug('passing args to the nested router ...');
o = extend(true, {}, o);
const subroutine = this.subroutine;
assert('string' === typeof o.params[o.subroutine], 'subroutine not found!');
o.path = o.params[o.subroutine];
if (o.path[0] != '/') o.path = '/' + o.path;
delete o.params[o.subroutine];
delete o.subroutine;
function name2routePath (name, options) {
debug('Router: convert name to route path : ' + name);
if ('string' !== typeof name) {
throw new Error('Name must be a string');
// nesting match will add a count on the route counter
// but it is not really routed, so -1 to fix the counter
req.routed--;
return [o, req, ...args];
}
if (name === (options.default || 'index.js')) {
return '';
// @overwrite switcher
_execute (result, o, req, ...args) {
req.params = $.cloneDeep(o.params);
return result(req, ...args);
}
const extname = path.extname(name);
if (extname && extname !== '.js') {
return false;
}
name = path.basename(name, extname);
if (!name || name[0] === '.') {
return false;
}
configure (options = {}) {
debug('configuring ...');
assert(options instanceof Object, 'Options must be an object!');
const prefix = options.param_prefix || '_';
const prefix_esc = escapeRegexp(prefix);
this._config = this._config instanceof Object ? this._config : $.cloneDeep(Router.defaultConfig);
assert(this._config instanceof Object, 'Internal Error');
let routePath = name.replace(new RegExp("^" + prefix_esc + "(?!(" + prefix_esc + ")|$)"), ":")
.replace(new RegExp("^" + prefix_esc + prefix_esc), prefix);
if (!Array.isArray(options.methods) || options.methods.length <= 0) {
options.methods = methods;
}
debug('Router: convert result is ' + routePath);
return routePath;
}
this._config = extend(true, this._config, options);
assert(Array.isArray(this._config.methods), 'Methods must be an array!');
function loadRouteByFilename (router, filename, routePath, root) {
if ('string' !== typeof filename || 'string' !== typeof root) {
throw new Error('Invalid param to load by dirname');
this._httpMethods = $.cloneDeep(this._config.methods).map(o => o.toLowerCase());
this._specialMethods = ['all', 'routed', 'other'];
this._methods = $.cloneDeep(this._httpMethods).concat($.cloneDeep(this._specialMethods));
this.bindMethods();
return this;
}
debug('Router: loading file ' + filename);
if (path.extname(filename) !== '.js' || filename.length <= 3) {
return;
get methods () {
return $.cloneDeep(this._methods);
}
get subroutine () {
return this._config.subroutine || 'subroutine';
}
route (method, pathexp, handler) {
debug('setting route for [' + method + '] [' + pathexp + '] ...');
assert('string' === typeof method, 'Method must be a string!');
method = method.toLowerCase();
assert(this._methods.includes(method), 'Invalid Method!');
assert('string' === typeof pathexp || pathexp instanceof RegExp, 'Path expression must be a string or a Regular Expression!');
debug("Router: route path [" + routePath + "]");
const absolutePath = path.join(root, filename);
//import fileModule from absolutePath;
let fileModule = null;
try {
fileModule = require(absolutePath).default || require(absolutePath);
let nesting = false;
if (handler instanceof Router) {
handler = handler._switcher;
nesting = true;
if (this._config['nesting-path-auto-complete'] && !(pathexp instanceof RegExp)) {
if (!pathexp.endsWith('/')) pathexp += '/';
pathexp += `:${this.subroutine}*`;
}
}
const regexp = path2regexp(pathexp);
return this._switcher.case({ method, pathexp, regexp, nesting }, handler, { break: false });
}
catch (e) {
console.log("Failed to load route by file '" + absolutePath + "', error is " + e.message);
return;
}
routes () {
return (req, ...args) => {
assert('string' === typeof req.url, 'URL must be a string');
assert('string' === typeof req.method, 'METHOD must be a string');
debug(`routing ${req.method.toUpperCase()} ${req.url} ...`);
const o = {
path: decodeURIComponent(req.url.split('?')[0]),
method: req.method.toLowerCase(),
params: {}
};
assert(this._httpMethods.includes(o.method), 'Invalid METHOD [' + req.method + ']');
if (fileModule instanceof Function) {
debug("Router: module is a function, use it to handle router directly");
let subRouter = Router.create(router.opts);
let result = fileModule(subRouter);
if (result instanceof Router) {
subRouter = result;
req.routed = 0;
return this._switcher.dispatch(o, req, ...args).catch(e => {
this.emit('error', e, req, ...args);
});
}
router.use(routePath, subRouter.routes());
}
else if (fileModule instanceof Object) {
loadByModule(router, routePath, fileModule);
clear () {
debug('clearing ...');
let method = null;
while (method = this._enabledMethods.pop()) {
delete this[method];
}
this._switcher._cases = [];
return this;
}
else {
throw new Error('Invalid router module');
bindMethods () {
debug('binding methods [' + $.truncate(this.methods.join(', '), 20) + '](' + this.methods.length + ' methods) ...');
this.clear();
for (let item of this.methods) {
const Method = item[0].toUpperCase() + item.slice(1).toLowerCase();
const method = Method.toLowerCase();
const METHOD = Method.toUpperCase();
this[method] = this[Method] = this[METHOD] = (pathexp, handler) => {
return this.route(method, pathexp, handler);
}
this._enabledMethods.push(method, Method, METHOD);
}
}
}
function loadByModule (router, routePath, module) {
debug("Router: load route by module");
//handle redirect routes
for (const method_ori in module) {
const METHOD = method_ori.toUpperCase();
if (METHOD !== 'REDIRECT' || 'string' !== typeof module[method_ori]) {
continue;
load (filename) {
assert('string' === typeof filename, 'File name must be a string!');
debug('loading router by path ...');
filename = path.normalize(filename).replace(/\\/g, '/');
if (!path.isAbsolute(filename)) {
const rootDirectory = path.dirname(process.mainModule.filename);
filename = path.join(rootDirectory, filename);
}
const desc = METHOD + ' ' + (router.opts.routePrefix || '') + routePath;
debug("Router: add router " + chalk.yellow(desc + " => " + module[method_ori]));
router.redirect(routePath, module[method_ori]);
return;
assert(path.isAbsolute(filename), 'File path must be or can be converted into an absolute path!');
debug('path is ' + filename);
const stat = fs.statSync(filename);
if (stat.isFile()) {
debug('loading router by file ...');
const filemodule = require(filename);
assert(filemodule instanceof Function || filemodule instanceof Object, 'File as router should export a Function or an Object!');
if (filemodule instanceof Function) {
debug('file module is a function');
const router = filemodule(this) || this;
assert(router instanceof Router, 'Function as router should return a Router or null');
if (router !== this) {
this.all('/', router);
}
}
else {
debug('file module is an object');
for (let method in filemodule) {
this.route(method, '/', filemodule[method]);
}
}
}
else {
debug('loading router by directory ...');
const files = fs.readdirSync(filename);
for (let file of files) {
const filepath = path.join(filename, file);
const router = new Router();
router.adapter = this.adapter;
router.load(filepath);
file = path.basename(file, path.extname(file));
const prefix = this.adapter.parseFileName(file) || file;
this.all('/' + prefix, router);
}
}
return this;
}
}
//handle methods
for (const method_ori in module) {
const method = method_ori.toLowerCase();
const METHOD = method_ori.toUpperCase();
const defaultConfig = {
max: 0, // max routed limit, 0 refers to unlimited
methods: methods, // methods to support
subroutine: 'subroutine', // subroutine param name for router's nesting
'nesting-path-auto-complete': true, // whether if auto complete the route path for nesting routes
}
const desc = METHOD + ' ' + (router.opts.routePrefix || '') + routePath;
if (!(module[method_ori] instanceof Function) || router.methods.indexOf(METHOD) < 0) {
continue;
}
debug("Router: add router " + chalk.yellow(desc));
router[method](routePath, module[method_ori]);
function defaultParseFileName (filename) {
let name = filename;
const PARAM = '.as.param';
const ASTERISK = '.as.asterisk';
if (filename.endsWith(PARAM)) {
name = ':' + filename.slice(0, filename.length - PARAM.length);
}
else if (filename.endsWith(ASTERISK)) {
name = ':' + filename.slice(0, filename.length - ASTERISK.length) + '*';
}
return name;
}
export default Router;
debug('Router: load ok');
debug('loaded!');
module.exports = Router;
{
"name": "lark-router",
"version": "1.0.2",
"description": "An koa route initialization and configuration module.",
"main": ".easy",
"engines": {
"node": ">=4.0.0"
},
"scripts": {
"test": "cd .easy && rm -rf node_modules && ln -s ../node_modules node_modules && NODE_ENV=testing ./node_modules/.bin/mocha --require should test/"
},
"repository": {
"type": "git",
"url": "https://github.com/larkjs/lark-router"
},
"keywords": [
"bootstrap",
"koa"
],
"author": "mdemo",
"license": "MIT",
"bugs": {
"url": "https://github.com/larkjs/lark-router/issues"
},
"homepage": "https://github.com/larkjs/lark-router",
"dependencies": {
"chalk": "^1.1.1",
"debug": "^2.2.0",
"escape-string-regexp": "^1.0.3",
"extend": "^3.0.0",
"koa-router": "^6.0.0",
"methods": "^1.1.1",
"path-to-regexp": "^1.2.1"
},
"devDependencies": {
"chalk": "^1.1.1",
"easy-babel": "^1.0.1",
"koa": "~2.0.0-alpha.3",
"koa-convert": "^1.2.0",
"mocha": "~2.0.1",
"should": "~4.3.0",
"supertest": "~0.15.0"
},
"easy": {
"main": "index.js",
"scripts": {
"test": "NODE_ENV=testing ./node_modules/.bin/mocha --require should test/"
}
}
}
"name": "lark-router",
"version": "1.1.0",
"description": "An koa route initialization and configuration module.",
"main": "index.js",
"engines": {
"node": ">=6.4.0"
},
"scripts": {
"test": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- --require should --recursive test",
"_test": "./node_modules/.bin/mocha --require should test/"
},
"repository": {
"type": "git",
"url": "https://github.com/larkjs/lark-router"
},
"keywords": [
"router",
"koa"
],
"author": "Sun Haohao",
"license": "MIT",
"bugs": {
"url": "https://github.com/larkjs/lark-router/issues"
},
"homepage": "https://github.com/larkjs/lark-router",
"dependencies": {
"debug": "^2.2.0",
"extend": "^3.0.0",
"lodash": "^4.15.0",
"methods": "^1.1.2",
"path-to-regexp": "^1.5.3",
"switch-case": "^0.4.0"
},
"devDependencies": {
"istanbul": "^0.4.5",
"koa": "^2.0.0-alpha.6",
"lodash": "^4.16.2",
"mocha": "~2.0.1",
"should": "~4.3.0",
"supertest": "^2.0.0"
}
}

@@ -9,88 +9,101 @@ lark-router

## Installation
## Install
```
$ npm install lark-router
$ npm install --save lark-router
```
## API
### `app.use(new Router().load('controllers').routes())`
## Get started
Exmaple:
_Lark-Router_ is a flexible and easy-to-use url router tool, compatible with native http apps, express apps and koa(v2) apps.
* http apps
```
import Koa from 'koa';
import Router from 'lark-router';
const router = new LarkRouter();
const router = new Router().load('controllers');
router.get('/foo/bar', (req, res) => res.end("/foo/bra requested!"));
router.on('error', (error, req, res) => {
res.statusCode = 500;
res.end(error.message);
});
const app = new Kao();
http.createServer(router.routes()).listen(3000);
```
app.use(router.routes());
* koa apps
```
const router = new LarkRouter();
const app = new Koa();
app.listen(3000);
router.get('/foo/bar', (ctx, next) => ctx.body = '/foo/bar requested!');
router.on('error', (error, ctx, next) => {
ctx.statusCode = 500;
ctx.body = error.message;
return next();
});
app.use(router.routes()).listen(3000);
```
## load
## Params
### routes
See [`path-to-regexp`](https://github.com/pillarjs/path-to-regexp). Params object is bind to the first argument of the app processor.
`lark-router` extends `koa-router` with a method `load(directory, prefix)`. By calling `router.load(directory, prefix)`, `lark-router` will load all js files recursively under that directory, and use their exports as callbacks to the routes corresponding to their paths.
```javascript
router.get('/:foo/:bar', (ctx, next) => { console.log(ctx.params); }); // ===> { foo: xxx, bar: xxx }
router.get(/^\/(\d+)\/(\w+)$/, (ctx, next) => { console.log(ctx.params); }); // ===> { 0: xxx, 1: xxx}
```
## all, other, routed
This is how file paths is converted into routes (with default options: `{ default: 'index.js', param_prefix: '_'}`)
Lark router has 3 special methods.
* all: match all requests
```
directory
├─ index.js => /
├─ hello/
│ └─ world.js => /hello/world
└─ _category/
└─ _title.js => /:category/:title
router.all('/foo/bar', handler); // ===> response to GET/POST/DELETE/... /foo/bar
```
#### methods
* other: match all unmatched requests
Methods should be defined in those js files, exported as verb properties. We recommand you use verbs in upper case to avoid using reserved words such as `delete`.
```
/**
* @file: hello/world.js
**/
export const GET = async (ctx, next) => {
// handle requests on GET /hello/world
}
router.other('*', response404notfound); // ===> response to GET/POST/DELETE/... /foo/bar if no other route matched
```
export const DELETE = async (ctx, next) => {
// handle request on DELETE /hello/world
}
* routed: match all matched requests
```
router.routed('/foo/bar', () => console.log('/foo/bar has been routed')); // ===> response to GET/POST/DELETE/... /foo/bar if some routes matched
```
or use `router` directly by exporting a function
## Nesting
You could nest routers together:
```
/**
* @file: hello/world.js
**/
mainRouter.all('/api', apiRouter);
```
export default router => {
router.get('/', async (ctx, next) => {
// handle requests on GET /hello/world
}
router.get('/:foo/:bar', async (ctx, next) => {
// handle requests on GET /hello/world/:foo/:bar
}
}
_Note that Lark-Router uses a path param to pass the unmatched part of path. That param name can be configured, usually is `subroutine`, and a string `'/:subroutine*'` will be append to that expression automatically._
```
mainRouter.configure({
'subroutine': 'sub',
'nesting-path-auto-complete': false,
});
## Tests
mainRouter.all('/api/:sub*', api); // equivalent to the example above.
```
npm test
## Async processors
For async processors, return promises.
```
router.get('/', () => new Promise(...));
```
## Loading files and directories to generate route rules
TBD...
## DETAILED DOC
TBD...
[npm-image]: https://img.shields.io/npm/v/lark-router.svg?style=flat-square

@@ -97,0 +110,0 @@ [npm-url]: https://npmjs.org/package/lark-router

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc