diff --git a/lib/utils/loadModule.js b/lib/utils/loadModule.js deleted file mode 100644 index 4f1adcb..0000000 --- a/lib/utils/loadModule.js +++ /dev/null @@ -1,24 +0,0 @@ -const pathUtil = require('path'); - -const EXTS = Object.keys(require.extensions); - -module.exports = function loadModule(path) { - path = resolve(path); - return path ? ensure(require(path)) : null; -}; - -function resolve(path) { - const ext = pathUtil.extname(path); - if (ext && !EXTS.includes(ext)) { - return null; - } - try { - return require.resolve(path, { paths: [] }); - } catch (e) { - return null; - } -} - -function ensure(mod) { - return ('default' in mod) ? mod.default : mod; -} diff --git a/package.json b/package.json index 0caa0ca..296fa66 100644 --- a/package.json +++ b/package.json @@ -1,42 +1,7 @@ { "name": "avada", - "version": "1.5.0-beta.1", - "description": "A nodejs web framework basedon koa.", - "main": "lib/index.js", - "scripts": { - "lint": "eslint lib", - "test": "echo pass!", - "ci": "npm run lint && npm run test" - }, - "dependencies": { - "@koa/cors": "^3.1.0", - "assert": "^2.0.0", - "co-busboy": "^1.4.1", - "debug": "^4.3.1", - "ejs": "^3.1.5", - "inflection": "^1.12.0", - "koa": "^2.13.0", - "koa-bodyparser": "^4.3.0", - "koa-compress": "^5.0.1", - "koa-conditional-get": "^3.0.0", - "koa-csrf": "^3.0.8", - "koa-etag": "^4.0.0", - "koa-favicon": "^2.1.0", - "koa-response-time": "^2.1.0", - "koa-session": "^6.1.0", - "koa-static": "^5.0.0", - "minimatch": "^3.0.4", - "mz": "^2.7.0", - "path-to-regexp": "^6.2.0", - "ramda": "^0.27.1" - }, - "devDependencies": { - "eslint": "^7.14.0", - "eslint-config-bce": "^7.0.0" - }, - "publishConfig": { - "registry": "https://registry.npmjs.org" - }, + "description": "A nodejs web framework", + "private": true, "repository": "git@github.com:bencode/avada.git", "author": "bencode ", "license": "MIT" diff --git a/.eslintrc b/packages/avada/.eslintrc similarity index 100% rename from .eslintrc rename to packages/avada/.eslintrc diff --git a/packages/avada/lib/components/container/index.js b/packages/avada/lib/components/container/index.js new file mode 100644 index 0000000..a104e7d --- /dev/null +++ b/packages/avada/lib/components/container/index.js @@ -0,0 +1,4 @@ +module.exports = function ControllerComponent(app) { + app.Container = new Map(); +}; + diff --git a/lib/components/controller/ControllerContainer.js b/packages/avada/lib/components/controller/ControllerContainer.js similarity index 100% rename from lib/components/controller/ControllerContainer.js rename to packages/avada/lib/components/controller/ControllerContainer.js diff --git a/lib/components/controller/index.js b/packages/avada/lib/components/controller/index.js similarity index 82% rename from lib/components/controller/index.js rename to packages/avada/lib/components/controller/index.js index abe969a..77ddebe 100644 --- a/lib/components/controller/index.js +++ b/packages/avada/lib/components/controller/index.js @@ -1,6 +1,6 @@ -const fs = require('fs'); const pathUtil = require('path'); -const loadModule = require('../../utils/loadModule'); +const { tryLoadModule } = require('../../utils/module'); +const scanAppModules = require('../../utils/scanAppModules'); const ControllerContainer = require('./ControllerContainer'); @@ -10,7 +10,7 @@ module.exports = function ControllerComponent(app, settings) { const controllerRoot = config.controllerRoot || pathUtil.join(appRoot, 'controllers'); const container = new ControllerContainer(); - app.controller = container.add.bind(container); + app.Controller = container; setupControllers(container, { controllerRoot }); @@ -20,12 +20,10 @@ module.exports = function ControllerComponent(app, settings) { function setupControllers(controller, { controllerRoot }) { - const rHidden = /^\..+/; - const list = fs.readdirSync(controllerRoot) - .filter(name => !rHidden.test(name)); + const list = scanAppModules(controllerRoot); for (const file of list) { const path = pathUtil.join(controllerRoot, file); - const mod = loadModule(path); + const mod = tryLoadModule(path); if (mod) { const name = pathUtil.basename(file, pathUtil.extname(file)); controller.add(name, mod); diff --git a/lib/components/core/errorHandler.js b/packages/avada/lib/components/core/errorHandler.js similarity index 96% rename from lib/components/core/errorHandler.js rename to packages/avada/lib/components/core/errorHandler.js index a698389..83a6047 100644 --- a/lib/components/core/errorHandler.js +++ b/packages/avada/lib/components/core/errorHandler.js @@ -10,7 +10,6 @@ module.exports = function createErrorHandler({ env }) { return next().catch(e => { const status = e.status || 500; ctx.app.emit('error', e, ctx); - global.console.error(e); const message = getMessage(env, e); ctx.status = status; if (ctx.is('application/json') || diff --git a/lib/components/core/index.js b/packages/avada/lib/components/core/index.js similarity index 100% rename from lib/components/core/index.js rename to packages/avada/lib/components/core/index.js diff --git a/lib/components/router/Router.js b/packages/avada/lib/components/router/Router.js similarity index 98% rename from lib/components/router/Router.js rename to packages/avada/lib/components/router/Router.js index 5e8965e..aa8e0e4 100644 --- a/lib/components/router/Router.js +++ b/packages/avada/lib/components/router/Router.js @@ -46,6 +46,7 @@ module.exports = class Router { * - query {Object} */ route(path, request) { + debug('try route %s', path); if (this.routes.length === 0) { return null; } diff --git a/lib/components/router/index.js b/packages/avada/lib/components/router/index.js similarity index 85% rename from lib/components/router/index.js rename to packages/avada/lib/components/router/index.js index 5b7d391..49c4c12 100644 --- a/lib/components/router/index.js +++ b/packages/avada/lib/components/router/index.js @@ -1,6 +1,6 @@ const pathUtil = require('path'); const debug = require('debug')('avada:router'); -const loadModule = require('../../utils/loadModule'); +const { tryLoadModule } = require('../../utils/module'); const pathToRegexp = require('../../utils/pathToRegexp'); const parse = require('./parse'); const AppRouter = require('./Router'); @@ -8,17 +8,20 @@ const AppRouter = require('./Router'); module.exports = function RouterComponent(app, settings) { const router = new AppRouter(); - app.router = router.add.bind(router); + app.Router = router; const appRoot = settings.applicationRoot; const config = settings.router || {}; const configRoot = config.configPath || pathUtil.join(appRoot, 'router'); - const fn = loadModule(configRoot); - const { middlewares, routes } = parse(fn); - - setupMiddlewares(app, middlewares); - setupRules(router, routes); + const fn = tryLoadModule(configRoot, { checkExists: false }); + if (typeof fn === 'function') { + const { middlewares, routes } = parse(fn); + setupMiddlewares(app, middlewares); + setupRules(router, routes); + } else { + global.console.error('router file not exits'); + } app.use(createRouter(router), { level: 2 }); }; diff --git a/lib/components/router/parse.js b/packages/avada/lib/components/router/parse.js similarity index 100% rename from lib/components/router/parse.js rename to packages/avada/lib/components/router/parse.js diff --git a/lib/components/service/ServiceContainer.js b/packages/avada/lib/components/service/ServiceContainer.js similarity index 100% rename from lib/components/service/ServiceContainer.js rename to packages/avada/lib/components/service/ServiceContainer.js diff --git a/lib/components/service/index.js b/packages/avada/lib/components/service/index.js similarity index 67% rename from lib/components/service/index.js rename to packages/avada/lib/components/service/index.js index 92e1af5..b1d29a9 100644 --- a/lib/components/service/index.js +++ b/packages/avada/lib/components/service/index.js @@ -1,12 +1,12 @@ -const fs = require('fs'); const pathUtil = require('path'); -const loadModule = require('../../utils/loadModule'); +const scanAppModules = require('../../utils/scanAppModules'); +const { tryLoadModule } = require('../../utils/module'); const ServiceContainer = require('./ServiceContainer'); module.exports = function ServiceComponent(app, settings) { const container = new ServiceContainer(app); - app.service = container.add.bind(container); + app.Service = container; const appRoot = settings.applicationRoot; const config = settings.service || {}; @@ -22,16 +22,13 @@ module.exports = function ServiceComponent(app, settings) { function setupServices(container, { serviceRoot }) { - const rHidden = /^\..+/; - const list = fs.readdirSync(serviceRoot) - .filter(name => !rHidden.test(name)); + const list = scanAppModules(serviceRoot); for (const file of list) { const name = pathUtil.basename(file, pathUtil.extname(file)); const path = pathUtil.join(serviceRoot, file); - const mod = loadModule(path); - const service = mod && mod.default ? mod.default : mod; + const service = tryLoadModule(path); if (service && typeof service === 'function') { - container.add(name, mod); + container.add(name, service); } } } diff --git a/packages/avada/lib/components/verify/index.js b/packages/avada/lib/components/verify/index.js new file mode 100644 index 0000000..d6ec5d2 --- /dev/null +++ b/packages/avada/lib/components/verify/index.js @@ -0,0 +1,12 @@ +const chalk = require('chalk'); + +module.exports = function VerifyComponent(app) { + const { routes } = app.Router; + for (const route of routes) { + const { rule } = route; + if (!app.Controller.has(rule.module, rule.action)) { + global.console.error(chalk.red(`action not exists: ${rule.module}#${rule.action}`)); + global.console.error('route detail %o', route); + } + } +}; diff --git a/lib/components/view/ViewContainer.js b/packages/avada/lib/components/view/ViewContainer.js similarity index 100% rename from lib/components/view/ViewContainer.js rename to packages/avada/lib/components/view/ViewContainer.js diff --git a/lib/components/view/index.js b/packages/avada/lib/components/view/index.js similarity index 92% rename from lib/components/view/index.js rename to packages/avada/lib/components/view/index.js index 3831f01..129daa4 100644 --- a/lib/components/view/index.js +++ b/packages/avada/lib/components/view/index.js @@ -7,15 +7,15 @@ const ViewContainer = require('./ViewContainer'); module.exports = function ViewComponent(app, settings) { const env = settings.env; const container = new ViewContainer({ env }); + app.View = container; - app.engine = container.engine.bind(container); - app.view = container.add.bind(container); app.context.render = async function(name, data) { this.body = await container.render(name, data); }; const config = settings.view || {}; - const viewRoot = config.viewRoot || pathUtil.join(settings.appRoot, 'views'); + const appRoot = settings.applicationRoot; + const viewRoot = config.viewRoot || pathUtil.join(appRoot, 'views'); installDefaultEngine(container, { env, config }); setupViews(container, { env, viewRoot }); }; diff --git a/lib/components/web/csrf.js b/packages/avada/lib/components/web/csrf.js similarity index 100% rename from lib/components/web/csrf.js rename to packages/avada/lib/components/web/csrf.js diff --git a/lib/components/web/flash.js b/packages/avada/lib/components/web/flash.js similarity index 100% rename from lib/components/web/flash.js rename to packages/avada/lib/components/web/flash.js diff --git a/lib/components/web/index.js b/packages/avada/lib/components/web/index.js similarity index 92% rename from lib/components/web/index.js rename to packages/avada/lib/components/web/index.js index 1cb4b02..6e181fd 100644 --- a/lib/components/web/index.js +++ b/packages/avada/lib/components/web/index.js @@ -1,12 +1,11 @@ +const { v4: uuid } = require('uuid'); const debug = require('debug')('avada:web'); const util = require('./util'); module.exports = function WebComponent(app, settings) { const config = settings.web || {}; - if (config.keys) { - initKeys(app, config.keys); - } + initKeys(app, config.keys); require('./params')(app); @@ -21,6 +20,10 @@ module.exports = function WebComponent(app, settings) { // private function initKeys(app, keys) { + if (!keys) { + keys = uuid(); + global.console.warn('app.keys missing, auto gen: %s', keys); + } keys = typeof keys === 'string' ? [keys] : keys; app.keys = keys; } diff --git a/lib/components/web/params.js b/packages/avada/lib/components/web/params.js similarity index 100% rename from lib/components/web/params.js rename to packages/avada/lib/components/web/params.js diff --git a/lib/components/web/security.js b/packages/avada/lib/components/web/security.js similarity index 100% rename from lib/components/web/security.js rename to packages/avada/lib/components/web/security.js diff --git a/lib/components/web/session.js b/packages/avada/lib/components/web/session.js similarity index 100% rename from lib/components/web/session.js rename to packages/avada/lib/components/web/session.js diff --git a/lib/components/web/util.js b/packages/avada/lib/components/web/util.js similarity index 100% rename from lib/components/web/util.js rename to packages/avada/lib/components/web/util.js diff --git a/lib/index.js b/packages/avada/lib/index.js similarity index 68% rename from lib/index.js rename to packages/avada/lib/index.js index 0c158f5..c66d9cf 100644 --- a/lib/index.js +++ b/packages/avada/lib/index.js @@ -3,12 +3,17 @@ const debug = require('debug')('avada:boot'); const Components = [ - 'core', 'web', 'router', 'controller', 'service', 'view' + 'core', 'web', 'container', + 'router', 'controller', 'service', 'view', + 'verify' ]; module.exports = function(config) { + config = normalizeConfig(config); const app = new Koa(); + let started = false; + let stopped = false; const list = []; for (const name of Components) { @@ -20,7 +25,13 @@ module.exports = function(config) { } app.start = async(opts = {}) => { + if (started) { + debug('already started, ignore'); + return null; + } + debug('app.start'); + started = true; for (const item of list) { if (typeof item.start === 'function') { debug('start component: %s', item.name); @@ -31,13 +42,24 @@ module.exports = function(config) { const { listen = true } = opts; if (listen) { const port = (config.server || {}).port || 3000; - const server = app.listen(port); - global.console.log(`server listen at: ${port}`); + const server = app.listen(port, () => { + global.console.log(`server listen at: ${port}`); + }); return server; } + return null; }; app.stop = async() => { + if (!started) { + throw new Error('app not started'); + } + if (stopped) { + debug('app stopped, ignore'); + return; + } + stopped = true; + debug('app.stop'); for (const item of list.reverse()) { if (typeof item.stop === 'function') { @@ -68,3 +90,11 @@ function makeArray(list) { return (list === undefined || list === null) ? [] : Array.isArray(list) ? list : [list]; } + +function normalizeConfig(config) { + return { + env: process.env.NODE_ENV || 'development', + applicationRoot: process.cwd(), + ...config + }; +} diff --git a/packages/avada/lib/utils/module.js b/packages/avada/lib/utils/module.js new file mode 100644 index 0000000..abac788 --- /dev/null +++ b/packages/avada/lib/utils/module.js @@ -0,0 +1,27 @@ +const fs = require('fs'); + +function tryLoadModule(path, opts = { checkExists: true }) { + if (checkModule(path, opts.checkExists)) { + return loadModule(path); + } + return null; +} + +function checkModule(path, checkExists) { + if (checkExists && !fs.existsSync(path)) { + return false; + } + try { + require.resolve(path); + return true; + } catch (e) { + return false; + } +} + +function loadModule(path) { + const mod = require(path); + return ('default' in mod) ? mod.default : mod; +} + +module.exports = { tryLoadModule, checkModule, loadModule }; diff --git a/lib/utils/pathToRegexp.js b/packages/avada/lib/utils/pathToRegexp.js similarity index 100% rename from lib/utils/pathToRegexp.js rename to packages/avada/lib/utils/pathToRegexp.js diff --git a/packages/avada/lib/utils/scanAppModules.js b/packages/avada/lib/utils/scanAppModules.js new file mode 100644 index 0000000..bac78af --- /dev/null +++ b/packages/avada/lib/utils/scanAppModules.js @@ -0,0 +1,31 @@ +const fs = require('fs'); +const pathUtil = require('path'); + +module.exports = function scanAppModules(dir) { + if (!fs.existsSync(dir)) { + return []; + } + const list = fs.readdirSync(dir); + return list.filter(name => isAppModule(name, dir)); +}; + +function isAppModule(name, dir) { + const rHidden = /^\./; + // ignore hidden file/dir + if (rHidden.test(name)) { + return false; + } + + // valid js identify + const rName = /^[a-zA-Z]\w*$/; + const path = pathUtil.join(dir, name); + const stats = fs.statSync(path); + if (stats.isDirectory()) { + return rName.test(name); + } + if (stats.isFile()) { + const basename = pathUtil.basename(name, pathUtil.extname(name)); + return rName.test(basename); + } + return false; +} diff --git a/packages/avada/package.json b/packages/avada/package.json new file mode 100644 index 0000000..cae3eb2 --- /dev/null +++ b/packages/avada/package.json @@ -0,0 +1,45 @@ +{ + "name": "avada", + "version": "1.5.0-beta.11", + "description": "A nodejs web framework basedon koa.", + "main": "lib/index.js", + "scripts": { + "lint": "eslint lib", + "test": "echo pass!", + "ci": "npm run lint && npm run test" + }, + "dependencies": { + "@koa/cors": "^3.1.0", + "assert": "^2.0.0", + "chalk": "^4.1.1", + "co-busboy": "^1.4.1", + "debug": "^4.3.1", + "ejs": "^3.1.5", + "inflection": "^1.12.0", + "koa": "^2.13.0", + "koa-bodyparser": "^4.3.0", + "koa-compress": "^5.0.1", + "koa-conditional-get": "^3.0.0", + "koa-csrf": "^3.0.8", + "koa-etag": "^4.0.0", + "koa-favicon": "^2.1.0", + "koa-response-time": "^2.1.0", + "koa-session": "^6.1.0", + "koa-static": "^5.0.0", + "minimatch": "^3.0.4", + "mz": "^2.7.0", + "path-to-regexp": "^6.2.0", + "ramda": "^0.27.1", + "uuid": "^8.3.2" + }, + "devDependencies": { + "eslint": "^7.14.0", + "eslint-config-bce": "^7.0.0" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org" + }, + "repository": "git@github.com:bencode/avada.git", + "author": "bencode ", + "license": "MIT" +} diff --git a/packages/avata-unit/lib/index.js b/packages/avata-unit/lib/index.js new file mode 100644 index 0000000..b07fdc8 --- /dev/null +++ b/packages/avata-unit/lib/index.js @@ -0,0 +1,19 @@ +const avada = require('avada'); +const request = require('supertest'); + +exports.createApp = function createApp(config) { + const app = avada(config); + const startDefer = app.start({ listen: false }); + + const start = async() => { + await startDefer; + } + + const agent = () => request.agent(app.callback()); + + const get = (...args) => { + return request.agent(app.callback()).get(...args); + }; + + return { start, agent, get }; +}; diff --git a/packages/avata-unit/package.json b/packages/avata-unit/package.json new file mode 100644 index 0000000..38c0994 --- /dev/null +++ b/packages/avata-unit/package.json @@ -0,0 +1,22 @@ +{ + "name": "avada-unit", + "version": "1.0.0-beta.2", + "description": "unit test utility for avada", + "main": "lib/index.js", + "scripts": { + "lint": "eslint lib", + "test": "echo pass!", + "ci": "npm run lint && npm run test" + }, + "dependencies": { + "avada": "^1.5.0-beta.4", + "supertest": "^6.1.3" + }, + "devDependencies": {}, + "publishConfig": { + "registry": "https://registry.npmjs.org" + }, + "repository": "git@github.com:bencode/avada.git", + "author": "bencode ", + "license": "MIT" +} diff --git a/packages/avata-util/.eslintrc b/packages/avata-util/.eslintrc new file mode 100644 index 0000000..dca6e0e --- /dev/null +++ b/packages/avata-util/.eslintrc @@ -0,0 +1,6 @@ +{ + extends: 'eslint-config-bce', + rules: { + 'import/prefer-default-export': 0 + } +} diff --git a/packages/avata-util/lib/param.js b/packages/avata-util/lib/param.js new file mode 100644 index 0000000..56a7c0d --- /dev/null +++ b/packages/avata-util/lib/param.js @@ -0,0 +1,39 @@ +const { curry } = require('ramda'); + +exports.parse = curry((define, query) => { + return Object.keys(define).reduce((acc, name) => { + const val = query[name]; + const [fn, def] = makeArray(define[name]); + acc[name] = typeof val === 'string' ? fn(val) : + typeof val === 'undefined' ? def : val; + return acc; + }, {}); +}); + +exports.types = { + number, string, bool, arrayOf +}; + +function number(val) { + return +val; +} + +function string(val) { + return '' + (val || ''); +} + +function bool(val) { + return val === 'true' || val === '1'; +} + +function arrayOf(fn) { + return val => { + return val.split(/,/).map(fn); + }; +} + +// Utility + +function makeArray(val) { + return Array.isArray(val) ? val : [val]; +} diff --git a/packages/avata-util/lib/param.test.js b/packages/avata-util/lib/param.test.js new file mode 100644 index 0000000..a8f1101 --- /dev/null +++ b/packages/avata-util/lib/param.test.js @@ -0,0 +1,26 @@ +const { parse, types } = require('./param'); + +test('parse', () => { + const query = { + pageIds: '1,3,5', + type: '3', + action: '1', + name: 'test', + define: 'product' + }; + const params = parse({ + pageIds: types.arrayOf(types.number), + type: types.number, + action: types.number, + name: types.string, + define: types.string + }, query); + + expect(params).toEqual({ + pageIds: [1, 3, 5], + type: 3, + action: 1, + name: 'test', + define: 'product' + }); +}); diff --git a/packages/avata-util/package.json b/packages/avata-util/package.json new file mode 100644 index 0000000..f2d5d16 --- /dev/null +++ b/packages/avata-util/package.json @@ -0,0 +1,25 @@ +{ + "name": "avada-util", + "version": "1.0.0-beta.1", + "description": "utility tools for application", + "main": "lib/index.js", + "scripts": { + "lint": "eslint lib", + "test": "jest lib", + "ci": "npm run lint && npm run test" + }, + "dependencies": { + "ramda": "^0.27.1" + }, + "devDependencies": { + "eslint": "^7.27.0", + "eslint-config-bce": "^7.0.0", + "jest": "^27.0.3" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org" + }, + "repository": "git@github.com:bencode/avada.git", + "author": "bencode ", + "license": "MIT" +}