├── .gitignore ├── .travis.yml ├── index.d.ts ├── package.json ├── lib └── helpers.js ├── test ├── lib │ └── helpers.test.js └── index.test.js ├── index.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | .env 3 | .idea 4 | .DS_Store 5 | coverage/* 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14" 4 | - "13" 5 | - "12" 6 | - "10" 7 | - "8" 8 | 9 | install: 10 | - npm install 11 | 12 | script: npm test 13 | 14 | after_success: 15 | - bash <(curl -s https://codecov.io/bash) 16 | - npm run coveralls 17 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Uses Route Magic 3 | * @param app The instantiated Express app object. 4 | * @param options Options. 5 | */ 6 | export function use(app: any, options?: { 7 | /** If `invokerPath` is not defined, this is relative to your nodejs ROOT. */ 8 | routesFolder?: string 9 | /** If this is defined, `routesFolder` will be relative to this path instead of your nodejs ROOT. */ 10 | invokerPath?: string 11 | /** Use your own debug module. */ 12 | debug?: function 13 | /** This prints out all your routes. If no debug module is passed, it uses console.log by default. */ 14 | logMapping?: boolean 15 | /** `false` by default, i.e. you should not have a `foo.js` and a folder named `foo` sitting at the same level. That's poor organisation. */ 16 | allowSameName?: boolean 17 | /** Allows you to skip folders or files with a suffix. */ 18 | ignoreSuffix?: string | string[] 19 | }): undefined 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-routemagic", 3 | "version": "2.0.8", 4 | "description": "A simple and fast, fire-and-forget module that all Nodejs+Express app should have, to automatically require all your express routes without bloating your code with `app.use('i/will/repeat/this', require('./i/will/repeat/this')`. 把 Express 路由图给自动化。", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha './test/' --recursive --exit", 8 | "cover": "istanbul cover _mocha -- './test/' --recursive --exit", 9 | "coveralls": "npm run cover -- --report lcovonly && cat ./coverage/lcov.info | coveralls" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/calvintwr/express-routemagic.git" 14 | }, 15 | "engines": { 16 | "node": ">=8.0.0" 17 | }, 18 | "keywords": [ 19 | "express", 20 | "router", 21 | "automatic", 22 | "require", 23 | "express route magic", 24 | "express-routemagic", 25 | "路由", 26 | "路由图" 27 | ], 28 | "author": "calvintwr", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/calvintwr/express-routemagic/issues" 32 | }, 33 | "homepage": "https://github.com/calvintwr/express-routemagic#readme", 34 | "dependencies": {}, 35 | "devDependencies": { 36 | "chai": "^4.2.0", 37 | "coveralls": "^3.1.0", 38 | "istanbul": "^0.4.5", 39 | "mocha": "^7.1.2", 40 | "mocha-lcov-reporter": "^1.3.0" 41 | }, 42 | "files": [ 43 | "index.js", 44 | "index.d.ts", 45 | "lib" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const util = require('util') 3 | 4 | // localised helpers 5 | function argFail(expect, got, name, note) { 6 | if (!Array.isArray(expect)) expect = [expect] 7 | got = _type(got) 8 | if (_found(got)) return false 9 | return _msg() 10 | 11 | function _found(got) { 12 | let found = expect.find(el => _vet(el) === got) 13 | return typeof found !== 'undefined' 14 | } 15 | 16 | function _msg() { 17 | let msg = 'Invalid Argument' 18 | msg += name ? ' ' + name : '' 19 | msg += `: Expect type ${_list(expect)} but got \`${got}\`.` 20 | msg += note ? ` Note: ${note}.` : '' 21 | return msg 22 | } 23 | 24 | function _vet(el) { 25 | const valid = [ 26 | 'string', 27 | 'number', 28 | 'array', 29 | 'object', 30 | 'function', 31 | 'boolean', 32 | 'null', 33 | 'undefined' 34 | // no support for symbol. should we care? 35 | ] 36 | if (typeof el !== 'string') throw new Error(`Internal error: Say what you expect to check as a string. Not ${el} with type \`${typeof el}\`.`) 37 | if (valid.indexOf(el) === -1) throw new Error(`Internal error: \`${el}\` is not a valid type to check for. Please use only ${_list(valid)}.`) 38 | return el 39 | } 40 | 41 | function _list(array) { 42 | return array.map(el => { 43 | return `\`${el}\`` 44 | }).join(' or ') 45 | } 46 | 47 | // get rid of all the problems typeof [] is `object`. 48 | function _type(got) { 49 | if (typeof got !== 'object') return typeof got 50 | if (Array.isArray(got)) return 'array' 51 | if (got === null) return 'null' 52 | return 'object' 53 | } 54 | } 55 | exports.argFail = argFail 56 | 57 | function applyOpts(obj, opts, props) { 58 | props.forEach(prop => { 59 | if (opts[prop] !== undefined) obj[prop] = opts[prop] 60 | }) 61 | } 62 | exports.applyOpts = applyOpts 63 | 64 | function optionsBC(payload, obj) { 65 | if (typeof payload !== 'object') throw new Error(`Internal error: optionsBC expects an \`object\`.`) 66 | if (obj.old === obj.new) throw new Error(`Internal error: The old and new properties are both named \`${obj.old}\`. Spelling mistake?`) 67 | if (typeof payload[obj.old] !== 'undefined') { 68 | util.deprecate(() => { 69 | payload[obj.new] = payload[obj.old] 70 | }, `\`${obj.old}\` is deprecated. Please use \`${obj.new}\` instead.`)() 71 | } 72 | } 73 | exports.optionsBC = optionsBC 74 | -------------------------------------------------------------------------------- /test/lib/helpers.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const hlp = require('../../lib/helpers.js') 4 | 5 | const should = require('chai').should() 6 | 7 | describe('argFail', () => { 8 | it('should throw error when nothing is passed in', () => { 9 | hlp.argFail.should.Throw(Error, "Internal error: Say what you expect to check as a string. Not undefined with type `undefined`.") 10 | }) 11 | it('should throw error when object is passed as `expect`', () => { 12 | (()=> { 13 | hlp.argFail(new Object()) 14 | }).should.Throw(Error, "Internal error: Say what you expect to check as a string. Not [object Object] with type `object`.") 15 | }) 16 | it('should throw error when non-string is passed in array as `expect`', () => { 17 | (()=> { 18 | hlp.argFail(['string', 1]) 19 | }).should.Throw(Error, "Internal error: Say what you expect to check as a string. Not 1 with type `number`.") 20 | }) 21 | it('should throw error when non-valid type as `expect`', () => { 22 | (()=> { 23 | hlp.argFail(['string', 'not-valid']) 24 | }).should.Throw(Error, "Internal error: `not-valid` is not a valid type to check for. Please use only `string` or `number` or `array` or `object` or `function` or `boolean` or `null` or `undefined`.") 25 | }) 26 | it('should return message when expect differs from got', () => { 27 | hlp.argFail('string', {}, 'name', 'note').should.equal('Invalid Argument name: Expect type `string` but got `object`. Note: note.') 28 | }) 29 | it('should return false when matches', () => { 30 | hlp.argFail('array', [], 'name', null).should.be.false 31 | }) 32 | it('should return false when matching null', () => { 33 | hlp.argFail('null', null).should.be.false 34 | }) 35 | it('should return message with multiple expecteds, if differ from got', () => { 36 | hlp.argFail(['string', 'array'], {}, null, 'note').should.equal('Invalid Argument: Expect type `string` or `array` but got `object`. Note: note.') 37 | }) 38 | }) 39 | 40 | describe('applyOpts', () => { 41 | it('should apply props name in array', () => { 42 | let obj = {} 43 | hlp.applyOpts(obj, { foo: 'bar', hoo: 'rah'}, ['foo']) 44 | obj.should.have.property('foo') 45 | obj.should.not.have.property('hoo') 46 | }) 47 | }) 48 | 49 | describe('optionsBC', () => { 50 | it('should throw error when the old and new property names are the same.', () => { 51 | (()=> { 52 | hlp.optionsBC({}, {old: 'same-name', new: 'same-name'}) 53 | }).should.Throw(Error, `Internal error: The old and new properties are both named \`same-name\`. Spelling mistake?`) 54 | }) 55 | it('should transfer old prop to new prop.', () => { 56 | let payload = {oldName: 'value'} 57 | hlp.optionsBC(payload, { old: 'oldName', new: 'newName' }) 58 | payload.should.have.deep.equal({ oldName: 'value', newName: 'value' }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const magic = require('../index.js') 4 | 5 | const should = require('chai').should() 6 | const path = require('path') 7 | 8 | describe('routesFolder', () => { 9 | let magic_routesFolder = Object.create(magic) 10 | it('should remove trailing slash', () => { 11 | magic_routesFolder.routesFolder = path.normalize('./my/routes/folder/') 12 | magic_routesFolder.routesFolder.should.equal(path.normalize('./my/routes/folder')) 13 | }) 14 | }) 15 | 16 | describe('ignoreSuffix', () => { 17 | let magic_ignoreSuffix = Object.create(magic) 18 | it('should make string become array', () => { 19 | magic_ignoreSuffix.ignoreSuffix = 'test' 20 | magic_ignoreSuffix.ignoreSuffix.should.be.an('array').have.lengthOf(1).have.members(['test']) 21 | }) 22 | }) 23 | 24 | describe('use', () => { 25 | it('should throw error if no app (1st argument) is passed in', () => { 26 | (()=> { 27 | magic.use() 28 | }).should.Throw(Error, 'Invalid argument: Express `app` instance must be passed in as 1st argument.') 29 | }) 30 | 31 | it('should accept 2nd argument as routesFolder if it is a `string`', () => { 32 | let magic_use = Object.create(magic) 33 | try { magic_use.use(function(){}, 'testFolder') } catch (error) {} 34 | magic_use.routesFolder.should.equal('testFolder') 35 | }) 36 | it('should accept 2nd argument as object', () => { 37 | let magic_use = Object.create(magic) 38 | let debug = function () {} 39 | let options = { 40 | ignoreSuffix: ['test1', 'test2'], 41 | allowSameName: true, 42 | debug, 43 | logMapping: true 44 | } 45 | try { magic_use.use(function(){}, options) } catch (error) {} 46 | magic_use.should.deep.include({ routesFolder: 'routes' }) 47 | }) 48 | }) 49 | 50 | describe('scan', () => { 51 | let directory = path.join(__dirname, './../../', 'folder-not-exist') 52 | it('should throw error if routes directory does not exist', () => { 53 | (()=> { 54 | magic.scan(directory) 55 | }).should.Throw(Error, `Routes folder not found in: ${directory}`) 56 | }) 57 | }) 58 | 59 | describe('push', () => { 60 | it('should push payload into array', () => { 61 | let array = [] 62 | magic.push(array, 'payload') 63 | array.should.have.members(['payload']) 64 | magic.push(array, 'payload2') 65 | array.should.have.members(['payload', 'payload2']) 66 | }) 67 | }) 68 | 69 | describe('toIgnore', () => { 70 | let magic_toIgnore = Object.create(magic) 71 | magic_toIgnore.ignoreSuffix = ['ignore', 'dev'] 72 | it('should be false when payload matches', () => { 73 | magic_toIgnore.toIgnore('ignore', true).should.be.true 74 | magic_toIgnore.toIgnore('dev.ts').should.be.true 75 | magic_toIgnore.toIgnore('dev.js').should.be.true 76 | }) 77 | it('should be true when payload don\'t match', () => { 78 | magic_toIgnore.toIgnore('no-match').should.be.false 79 | }) 80 | }) 81 | 82 | describe('checkConflict', () => { 83 | it('should not throw error when file and folder names don\'t conflict', () => { 84 | function checkConflict() { 85 | let files = ['no-conflict.js', 'b.js'] 86 | let folders = ['conflict', 'd'] 87 | magic.checkConflict(files, folders, 'foo') 88 | } 89 | checkConflict.should.not.Throw(Error) 90 | }) 91 | it('should throw error when file and folder names conflict', () => { 92 | function checkConflict() { 93 | let files = ['conflict.js', 'b.js'] 94 | let folders = ['conflict', 'd'] 95 | magic.checkConflict(files, folders, 'foo') 96 | } 97 | checkConflict.should.Throw(Error, /Folder and file with conflict name: `conflict` in directory: `foo`./) 98 | }) 99 | }) 100 | 101 | describe('apiDirectory', () => { 102 | let magic_apiDir = Object.create(magic) 103 | it('should out correct api directory', () => { 104 | magic_apiDir.invokerPath = path.normalize('/with/trailing/slash/') 105 | magic_apiDir.routesFolder = path.normalize('./with/dot-slash/') 106 | magic_apiDir.apiDirectory('/with/trailing/slash/with/dot-slash/my-api-folder').should.equal(path.normalize('/my-api-folder')) 107 | }) 108 | it('should out correct api directory without trailling slash', () => { 109 | magic_apiDir.invokerPath = path.normalize('/with/trailing/slash/') 110 | magic_apiDir.routesFolder = path.normalize('./with/dot-slash/') 111 | magic_apiDir.apiDirectory('/with/trailing/slash/with/dot-slash/my-api-folder/').should.equal(path.normalize('/my-api-folder')) 112 | }) 113 | it('should out correct api directory `/` for outermost index.js file', () => { 114 | magic_apiDir.invokerPath = path.normalize('/with/trailing/slash/') 115 | magic_apiDir.routesFolder = path.normalize('./with/dot-slash/') 116 | magic_apiDir.apiDirectory('/with/trailing/slash/with/dot-slash/').should.equal(path.normalize('/')) 117 | }) 118 | }) 119 | 120 | describe('apiPath', () => { 121 | it('should out correct apiPath for non-index files', () => { 122 | magic.apiPath('bar.js', '/api').should.equal('/api/bar') 123 | magic.apiPath('bar.ts', '/api').should.equal('/api/bar') 124 | magic.apiPath('index.ts', '/api').should.equal('/api') 125 | }) 126 | it('should out correct apiPath for index files', () => { 127 | magic.apiPath('index.ts', '/api').should.equal('/api') 128 | magic.apiPath('index.js', '/api').should.equal('/api') 129 | }) 130 | }) 131 | 132 | describe('absolutePathToRoutesFolder', () => { 133 | let magic_absPath = Object.create(magic) 134 | it('should out correct absolute path -1', () => { 135 | magic_absPath.invokerPath = path.normalize('/with/trailing/slash/') 136 | magic_absPath.routesFolder = path.normalize('./with/dot-slash/') 137 | magic_absPath.absolutePathToRoutesFolder().should.equal(path.normalize('/with/trailing/slash/with/dot-slash')) 138 | }) 139 | it('should out correct absolute path -2', () => { 140 | magic_absPath.invokerPath = path.normalize('/without/trailing/slash') 141 | magic_absPath.routesFolder = path.normalize('../with/dot-dot') 142 | magic_absPath.absolutePathToRoutesFolder().should.equal(path.normalize('/without/trailing/with/dot-dot')) 143 | }) 144 | }) 145 | 146 | describe('absolutePathFile', () => { 147 | it('should out correct relative path', () => { 148 | magic.absolutePathFile('dir', 'file.js').should.equal(path.normalize('dir/file.js')) 149 | }) 150 | }) 151 | 152 | describe('pathRelativeToInvoker', () => { 153 | let magic_pathRelative = Object.create(magic) 154 | it('should out correct relative path', () => { 155 | magic_pathRelative.invokerPath = path.normalize('/app-folder/invoker/') 156 | magic_pathRelative.pathRelativeToInvoker('/app-folder/routes/dir', 'file.js').should.equal(path.normalize('../routes/dir/file.js')) 157 | }) 158 | }) 159 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Express Route-Magic v2.0.8 3 | * (c) 2021 Calvin Tan 4 | * Released under the MIT License. 5 | */ 6 | 'use strict' 7 | 8 | // modules 9 | const fs = require('fs') 10 | const path = require('path') 11 | const hlp = require('./lib/helpers.js') 12 | 13 | // defaults 14 | const Magic = { 15 | _moduleName: 'express-routemagic', 16 | _routesFolder: 'routes', 17 | _allowSameName: false, 18 | _debug: console.log, 19 | _logMapping: false, 20 | _invokerPath: path.join(__dirname, './../../') 21 | } 22 | 23 | // properties that require getters and setters 24 | Object.defineProperties(Magic, { 25 | 26 | invokerPath: { 27 | get() { 28 | return this._invokerPath 29 | }, 30 | set(val) { 31 | let fail = hlp.argFail('string', val, 'invokerPath', 'The path of where you invoked magic must be a valid `string`. Typically it is `__dirname`.') 32 | if (fail) throw new Error(fail) 33 | this._invokerPath = val 34 | } 35 | }, 36 | 37 | routesFolder: { 38 | get() { 39 | return this._routesFolder 40 | }, 41 | set(val) { 42 | let fail = hlp.argFail('string', val, 'routesFolder', 'This value defaults to \'routes\'. If you change your folder structure to follow you won\'t need this option.') 43 | if (fail) throw new Error(fail) 44 | if (val[val.length - 1] === '/' || val[val.length - 1] === '\\') val = val.substring(0, val.length - 1) 45 | this._routesFolder = val 46 | } 47 | }, 48 | 49 | ignoreSuffix: { 50 | get() { 51 | return this._ignoreSuffix 52 | }, 53 | set(val) { 54 | let fail = hlp.argFail(['string', 'array'], val, 'ignoreSuffix') 55 | if (fail) throw new Error(fail) 56 | this._ignoreSuffix = Array.isArray(val) ? val : [val] 57 | } 58 | }, 59 | 60 | allowSameName: { 61 | get() { 62 | return this._allowSameName 63 | }, 64 | set(val) { 65 | let fail = hlp.argFail('boolean', val, 'allowSameName') 66 | if (fail) throw new Error(fail) 67 | this._allowSameName = val 68 | } 69 | }, 70 | 71 | logMapping: { 72 | get() { 73 | return this._logMapping 74 | }, 75 | set(val) { 76 | let fail = hlp.argFail('boolean', val, 'logMapping') 77 | if (fail) throw new Error(fail) 78 | this._logMapping = val 79 | } 80 | }, 81 | 82 | debug: { 83 | get() { 84 | return this._debug 85 | }, 86 | set(val) { 87 | let fail = hlp.argFail('function', val, 'debug') 88 | if (fail) throw new Error(fail) 89 | this._debug = val 90 | } 91 | } 92 | }) 93 | 94 | // methods 95 | Magic.use = function(app, relativeRoutesFolderOrOptions) { 96 | 97 | if (!app) throw new Error('Invalid argument: Express `app` instance must be passed in as 1st argument.') 98 | this.app = app 99 | 100 | if (!hlp.argFail('string', relativeRoutesFolderOrOptions)) { 101 | 102 | this.routesFolder = path.normalize(relativeRoutesFolderOrOptions) 103 | 104 | } else if (!hlp.argFail('object', relativeRoutesFolderOrOptions)) { 105 | 106 | let options = relativeRoutesFolderOrOptions 107 | 108 | // may need debugging module downstream, so assign first. 109 | if (options.debug) this.debug = options.debug 110 | 111 | if (relativeRoutesFolderOrOptions.routesFolder) relativeRoutesFolderOrOptions.routesFolder = path.normalize(relativeRoutesFolderOrOptions.routesFolder) 112 | if (relativeRoutesFolderOrOptions.invokerPath) relativeRoutesFolderOrOptions.invokerPath = path.normalize(relativeRoutesFolderOrOptions.invokerPath) 113 | 114 | hlp.applyOpts(this, options, [ 115 | 'routesFolder', 116 | 'ignoreSuffix', 117 | 'allowSameName', 118 | 'debug', 119 | 'logMapping', 120 | 'invokerPath' 121 | ]) 122 | } 123 | 124 | this.scan(this.absolutePathToRoutesFolder()) 125 | } 126 | 127 | Magic.scan = function(directory) { 128 | let _folders = [] 129 | let _files = [] 130 | 131 | if (!fs.existsSync(directory)) throw new Error(`Routes folder not found in: ${directory}`) 132 | 133 | fs.readdirSync(directory).filter(file => { 134 | 135 | // ignore hidden file 136 | if (file.indexOf('.') === 0) return false 137 | 138 | // directory 139 | if (fs.lstatSync(path.join(directory, '/', file)).isDirectory()) { 140 | this.push(_folders, file, true) 141 | return false 142 | } 143 | 144 | // js files 145 | return ( 146 | (file.indexOf('.js') === file.length - '.js'.length) || 147 | (file.indexOf('.ts') === file.length - '.ts'.length) 148 | ) 149 | 150 | }).forEach(file => { 151 | if (['index.js', 'index.ts'].indexOf(file) > -1) { 152 | _files.unshift(file) 153 | } else { 154 | this.push(_files, file) 155 | } 156 | }) 157 | 158 | this.checkConflict(_files, _folders, directory) 159 | 160 | // require 161 | this.require(directory, _files) 162 | 163 | // scan folders 164 | _folders.forEach(folder => { 165 | this.scan(path.join(directory, '/', folder)) 166 | }) 167 | } 168 | 169 | Magic.push = function(array, payload, isDirectory) { 170 | if (!this.toIgnore(payload, isDirectory)) array.push(payload) 171 | } 172 | 173 | Magic.toIgnore = function(payload, isDirectory) { 174 | if (!isDirectory) payload = payload.substring(0, payload.length - 3) // remove the extension 175 | let toIgnore = false 176 | if (this.ignoreSuffix) { 177 | this.ignoreSuffix.forEach(suffix => { 178 | if (payload.indexOf(suffix) !== -1 && payload.indexOf(suffix) === payload.length - suffix.length) { 179 | toIgnore = true 180 | return null 181 | } 182 | }) 183 | } 184 | return toIgnore 185 | } 186 | 187 | Magic.checkConflict = function(files, folders, directory) { 188 | if (this.allowSameName) return false 189 | files.forEach(file => { 190 | if (folders.indexOf(file.substring(0, file.length - 3)) !== -1) throw new Error(`Folder and file with conflict name: \`${file.substring(0, file.length - 3)}\` in directory: \`${directory}\`.`) 191 | }) 192 | } 193 | 194 | Magic.require = function(dir, files) { 195 | let apiDirectory = this.apiDirectory(dir) 196 | files.forEach(file => { 197 | let apiPath = this.apiPath(file, apiDirectory) 198 | let route = require(this.absolutePathFile(dir, file)) 199 | // ES6 export defauly compatibility 200 | if (typeof route !== 'function') route = route.default 201 | this.app.use(apiPath, route) 202 | if (this.logMapping) this.debug(apiPath + ' => .' + this.pathRelativeToInvoker(dir, file)) 203 | }) 204 | } 205 | 206 | Magic.apiDirectory = function(dir) { 207 | let apiDir = path.relative(this.absolutePathToRoutesFolder(), dir) 208 | return (apiDir.length === 0) ? path.normalize('/') : path.normalize(path.join('/', apiDir)) 209 | } 210 | Magic.apiPath = function(file, apiDir) { 211 | apiDir = path.normalize(apiDir) 212 | // TODO: To support passing array to 213 | // have to check whether the apiDir have any commas. if yes can indicate a ['/route1', '/route2'] kind. 214 | // also need to check if file have any commans. if yes can indicate a ['/route1/filename1', '/route2/filename1', '/route1/filename2', '/route2/filename2'] kind of situation. 215 | let apiPath = (['index.js', 'index.ts'].indexOf(file) > -1) ? apiDir : path.join(apiDir, file.substring(0, file.length - 3)) 216 | apiPath = apiPath.replace(/\\/g, '/') 217 | return apiPath 218 | } 219 | Magic.absolutePathToRoutesFolder = function() { 220 | return path.join(this.invokerPath, this.routesFolder) 221 | } 222 | Magic.absolutePathFile = function(dir, file) { 223 | return path.join(dir, file) 224 | } 225 | Magic.pathRelativeToInvoker = function (dir, file) { 226 | return path.join(path.relative(this.invokerPath, dir), file) 227 | } 228 | 229 | module.exports = Object.create(Magic) 230 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![npm version](https://img.shields.io/npm/v/express-routemagic.svg?style=flat-square)](https://www.npmjs.com/package/express-routemagic) 4 | [![Build Status](https://badgen.net/travis/calvintwr/express-routemagic?style=flat-square)](https://travis-ci.com/calvintwr/express-routemagic) 5 | [![Coverage Status](https://badgen.net/coveralls/c/github/calvintwr/express-routemagic?style=flat-square)](https://coveralls.io/r/calvintwr/express-routemagic) 6 | [![license](https://img.shields.io/npm/l/express-routemagic.svg?style=flat-square)](https://www.npmjs.com/package/express-routemagic) 7 | [![install size](https://badgen.net/packagephobia/install/express-routemagic?style=flat-square)](https://packagephobia.now.sh/result?p=express-routemagic) 8 | 9 | # Route Magic 10 | >Route Magic is a simple and fast "implement-and-forget" routing module all Nodejs+Expressjs set ups should have. 11 | 12 | **Why? Because your routes folder structure is almost always your intended api URI structure; it ought to be automated, but it hasn't been. So Route Magic will do just that to invoke your routings based on file structure**. Drastically reduce unnecessary code -- keep your express app clean and simple, exactly like how it should be. This module has no dependencies. 13 | 14 | Route Magic是一个简单而又快速的Nodejs模块。它可自动化广泛使用的[Expressjs框架](https://github.com/expressjs/express)的路由图,因为**您的路由文件夹结构几乎都是您想要的API URI结构。Route Magic将根据您的文件夹结构自动调用路由。** 它保持 Express 简洁几明了的结构。该模块不依赖其它模块。 15 | 16 | ## Installation 17 | 18 | ``` 19 | npm i express-routemagic --save 20 | ``` 21 | For example, go [here](https://github.com/calvintwr/express-routemagic-eg). 22 | 23 | ## Usage 24 | 25 | ### Require syntax 26 | ```js 27 | // this file is app.js 28 | const magic = require('express-routemagic') 29 | magic.use(app) 30 | // Note: this assumes your routing files to be in `./routes` relative to the ROOT of your nodejs app. 31 | ``` 32 | 33 | ### ES6 syntax 34 | ```js 35 | import magic from 'express-routemagic' 36 | magic.use(app) 37 | ``` 38 | 39 | That's it! Continue to code everything else and let Magic take care of requiring your routes. 40 | (Note: Scroll to bottom for much more options.) 41 | 42 | ## Prologue 43 | The author's express app was simple at first. Right out of the box, everything seemed complete, nothing more was desired. But very quickly, it grew, and something felt amiss: the oddly familiar muscle reflex in performing ctrl c and p... 44 | 45 | ```js 46 | app.use('/', require('./routes/index')) 47 | app.use('/somePath', require('./routes/somePath')) 48 | app.use('/i/keep/repeating/myself', require('./routes/i/keep/repeating/myself')) 49 | //... and many more lines of repetitive code... 50 | ``` 51 | With every line, came the thought: “It's just one more”. But that didn't end; and the dissonances inevitably converge into a crescendo: 52 | 53 | >This doesn't make sense anymore. 54 | 55 | **_This was how Magic was born._** 56 | 57 | ## Say Hello to This 58 | 59 | You have already organised your files. So make that work for you: 60 | 61 | ```js 62 | // This shows all of magic's options 63 | magic.use(app, { 64 | routesFolder: './routes', // Optional. If `invokerPath` is not defined, this is relative to your nodejs ROOT. 65 | invokerPath: __dirname, // Optional. If this is defined, `routesFolder` will be relative to this path instead of your nodejs ROOT. 66 | debug: [ your own debug module ], // Optional 67 | logMapping: true, // Optional. This prints out all your routes. If no debug module is passed, it uses console.log by default 68 | allowSameName: false, // Optional. `false` by default, i.e. you should not have a `foo.js` and a folder named `foo` sitting at the same level. That's poor organisation. 69 | ignoreSuffix: string or array // Optional. Allows you to skip folders or files with a suffix. 70 | }) 71 | ``` 72 | Note: When getting started, enable `logMapping` to check your routings. The sequence which the routes are printed reflects the sequence the routes are invoked. **(Note:In general, for any given folder, it will invoke `index.js`, followed by other same-level `js` files in alphabetical order, followed by same-level folders (including its nested folders) in alphabetical order.)** 73 | 74 | To get started, you can generate a default Express app [using the CLI shipped by Express](https://expressjs.com/en/starter/generator.html) and modify its `app.js`. 75 | 76 | Or you can see an example [here](https://github.com/calvintwr/express-routemagic-eg). 77 | 78 | If your files are not in a default `routes` folder, you can define it for Magic: 79 | ```js 80 | magic.use(app, '[your routing directory relative to nodejs root]') // 'folder' is same as './folder' 81 | ``` 82 | 83 | ## Further Reading - How Does it Map the Routings? 84 | 85 | See an example app [here](https://github.com/calvintwr/express-routemagic-eg). 86 | 87 | ### Explanation 88 | 89 | Assuming the below file structure: 90 | 91 | ``` 92 | project-folder 93 | |--app 94 | |--routes 95 | | |--nested-folder 96 | | | |--bar 97 | | | | |--bar.js 98 | | | |--index.js 99 | | |--index.js 100 | | |--foo.js 101 | |--app.js 102 | ``` 103 | Route Magic is aware of your folder structure. Invoking Route Magic inside of `app.js` like this: 104 | ```js 105 | magic.use(app) 106 | ``` 107 | Is equivalent of doing all these automatically: 108 | ```js 109 | app.use('/', require('./routes/index.js')) 110 | app.use('/foo', require('./routes/foo.js')) 111 | app.use('/nested-folder', require('./routes/nested-folder/index.js')) 112 | app.use('/nested-folder/bar/bar', require('./routes/nested-folder/bar/bar.js')) // note the 2 bars here. 113 | ``` 114 | ### Recommended route files syntax 115 | You can either follow the syntax per generated by [express-generator](https://expressjs.com/en/starter/generator.html), or follow a slightly more updated syntax below in each of your routing `js` files: 116 | **ES6 syntax** 117 | ```js 118 | 'use strict' 119 | const router = require('express').Router() // still retained #require instead of #import due practicable compatibility. 120 | 121 | router.get('/', (req, res) => { res.send('You are in the root directory of this file.') }) 122 | module.exports = router 123 | ``` 124 | **ES5 syntax** 125 | ```js 126 | 'use strict' 127 | var router = require('express').Router() 128 | 129 | router.get('/', function(req, res) { res.send('You are in the root directory of this file.') }) 130 | module.exports = router 131 | ``` 132 | 133 | Note that '/' is always relative to the file structure. So if the above file is `routes/nested-folder/index.js`, the URL will be `https://domain:port/nested-folder`. This is the default Express behaviour and also automatically done for you by Magic. 134 | 135 | ### Routing Ordering Gotcha (a.k.a Express routing codesmells) 136 | The order of Express middleware (routings are middlewares as well) matters. Good express routing practices will negate surprises, whether you switch to Magic, or if you later re-organise/rename your folders. An example of bad express routing practice: 137 | 138 | ```js 139 | router.get('/:param', (req, res) => { 140 | // this should be avoided because this can block all routes invoked below it if it's invoked before them. 141 | }) 142 | 143 | // these will never get called 144 | router.get('/foo', (req, res) => { ... }) 145 | router.get('/bar', (req, res) => { ... }) 146 | ``` 147 | Instead, stick to the below and you will be fine: 148 | ```js 149 | router.get('/unique-name/:param', (req, res) => { 150 | // `unique-name` should be a terminal route, meaning it will not have any subpath under it. 151 | 152 | // this file should also not share the same name with any other folders that sits on the same level with it. 153 | // |-- i-dont-have-the-same-unique-name 154 | // | |-- index.js 155 | // | 156 | // |-- the-file-that-contains-the-route-with-unique-name.js 157 | }) 158 | 159 | // these will get called 160 | router.get('/foo', (req, res) => { ... }) 161 | router.get('/bar', (req, res) => { ... }) 162 | ``` 163 | 164 | ## More Examples - Additional Magic Options 165 | 166 | ```js 167 | const debug = require('debug')('your:namespace:magic') // some custom logging module 168 | 169 | magic.use(app, { 170 | routesFolder: './some-folder', 171 | debug: debug, 172 | logMapping: true, 173 | ignoreSuffix: '_bak' // Will ignore files like 'index_bak.js' or folders like 'api_v1_bak'. 174 | }) 175 | ``` 176 | You can also pass an array to `ignoreSuffix`: 177 | 178 | ```js 179 | magic.use(app, { 180 | ignoreSuffix: ['_bak', '_old', '_dev'] 181 | }) 182 | ``` 183 | Try it for yourself, go to [https://github.com/calvintwr/express-routemagic-eg](https://github.com/calvintwr/express-routemagic-eg). 184 | 185 | ### License 186 | 187 | Magic is MIT licensed. 188 | --------------------------------------------------------------------------------