From 8482f00cba7186edd19886f5a8536f215501f34b Mon Sep 17 00:00:00 2001 From: Nick Chisiu Date: Sun, 20 Aug 2017 16:32:59 +0300 Subject: [PATCH] initial release --- .editorconfig | 10 +++ .eslintignore | 7 ++ .eslintrc | 68 ++++++++++++++++ .gitignore | 3 +- .npmignore | 7 ++ .travis.yml | 27 ++++++ CHANGELOG.md | 2 + README.md | 31 ++++++- hooks.yml | 4 + index.js | 11 +++ lib/index.js | 144 ++++++++++++++++++++++++++++++++ lib/livereload.js | 29 +++++++ lib/watcher.js | 204 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 78 ++++++++++++++++++ 14 files changed, 623 insertions(+), 2 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 hooks.yml create mode 100644 index.js create mode 100644 lib/index.js create mode 100644 lib/livereload.js create mode 100644 lib/watcher.js create mode 100644 package.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0f09989 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..c48d420 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +**/*{.,-}min.js +**/node_modules/** +/_book +public/ +test-projects/ +/docs +coverage diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..59d4d23 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,68 @@ +{ + "extends": "airbnb", + "plugins": [], + "env": { + "node": true, + "browser": true, + "mocha": true + }, + "rules": { + "brace-style": [ + 2, + "1tbs", + { + "allowSingleLine": true + } + ], + "comma-dangle": [ + 2, + "never" + ], + "complexity": [ + 2, + 6 + ], + "curly": 2, + "eqeqeq": [ + 2, + "allow-null" + ], + "max-statements": [ + 2, + 30 + ], + "react/react-in-jsx-scope" : 0, + "react/jsx-filename-extension" : 0, + "arrow-body-style": 0, + "consistent-return": 0, + "no-shadow-restricted-names": 2, + "no-undef": 0, + "no-use-before-define": 2, + "radix": 2, + "semi": 2, + "space-infix-ops": 2, + "key-spacing": 0, + "no-multi-spaces": 0, + "import/extensions" : 0, + "object-shorthand": [ + 2, + "consistent" + ], + "no-param-reassign": 0, + "prefer-rest-params": 0, + "class-methods-use-this": 0, + "strict": 0, + "import/no-extraneous-dependencies": "off", + "import/no-dynamic-require": "off", + "react/require-extension": "off", + "import/no-unresolved" : "off" + }, + "globals": { + "AnalysisView": true, + "PollingView": true, + "Prism": true, + "Spinner": true, + "Timer": true, + "moment": true + } +} diff --git a/.gitignore b/.gitignore index 00cbbdf..77bdd92 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ # Logs -logs *.log npm-debug.log* yarn-debug.log* @@ -57,3 +56,5 @@ typings/ # dotenv environment variables file .env +# IntelliJ IDEA +.idea diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..70d6e4d --- /dev/null +++ b/.npmignore @@ -0,0 +1,7 @@ +# This file is to avoid `npm install` encounter chmod ENOENT error. +# Reference: http://stackoverflow.com/questions/17990647/npm-install-errors-with-error-enoent-chmod + +# Ignore there folders because they will not be used in production code. +# Reference: http://blog.xebia.com/2015/09/22/publishing-es6-code-to-npm/ +docs/ +md/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..21d4a7e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: node_js + +node_js: + - "6" + +services: + - docker + +before_install: + - pip install --user awscli + - export PATH=$PATH:$HOME/.local/bin + +install: + - travis_retry npm install + +script: + - set -e + - npm run eslint + - npm run jsinspect + +notifications: + webhooks: + urls: + - https://webhooks.gitter.im/e/3c87287517b9672eb7d9 + on_success: change + on_failure: always + on_start: never diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3a61b7b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +## v0.0.1 - 2017-08-18 +- initial release diff --git a/README.md b/README.md index 2b323d5..e0173f3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,31 @@ # squeezer-serve -Squeezer Serve Plugin . This plugin enables serving support for local development within the Squeezer Framework. Edit Add topics +Squeezer Serve Plugin . This plugin enables serving support for local development within the Squeezer Framework. + +[![Build Status](https://travis-ci.org/SqueezerIO/squeezer-serve.svg?branch=master)](https://travis-ci.org/SqueezerIO/squeezer-serve) +[![npm version](https://badge.fury.io/js/squeezer-serve.svg)](https://badge.fury.io/js/squeezer-serve) +[![npm version](https://badge.fury.io/js/squeezer-aws.svg)](https://badge.fury.io/js/squeezer-aws) +[![DUB](https://img.shields.io/dub/l/vibe-d.svg)]() + +### Installation + +`cd PROJECT_DIR` + +`npm i squeezer-serve --save` + +### Activate the plugin + +*PROJECT_DIR/squeezer.yml* + +```yaml +plugins: + - name: squeezer-serve + path: node_modules +``` + +```javascript +process.on('serveEvent', (event) => { + // event.req - express request hook + // event.res - express response hook + // event.data - called function data +}); +``` diff --git a/hooks.yml b/hooks.yml new file mode 100644 index 0000000..8561445 --- /dev/null +++ b/hooks.yml @@ -0,0 +1,4 @@ +- identifier : 'serve:run' + path : 'lib' + function : 'run' + diff --git a/index.js b/index.js new file mode 100644 index 0000000..f64a518 --- /dev/null +++ b/index.js @@ -0,0 +1,11 @@ +'use strict'; + +class ServePlugin { + constructor(sqz) { + this.sqz = sqz; + + this.commands = []; + } +} + +module.exports = ServePlugin; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..6a864c5 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,144 @@ +'use strict'; + +const _ = require('lodash'); +const fs = require('fs'); +const colors = require('colors'); +const express = require('express'); +const bodyParser = require('body-parser'); +const UrlPattern = require('url-pattern'); +const Watcher = require('./watcher'); +const Livereload = require('./livereload'); + +/** + * Class that serves a Squeezer project + */ +class Express { + constructor(sqz) { + this.sqz = sqz; + } + + /** + * Start the Node Express server + */ + run() { + return new Promise((resolve) => { + const app = express(); + const projectType = this.sqz.vars.project.type; + const projectPath = this.sqz.vars.project.path; + const project = this.sqz.vars.project; + let port = 4001; + + app.use((req, res, next) => { + res.set('Cache-Control', 'no-cache, private, no-store, must-revalidate, ' + + 'max-stale=0, post-check=0, pre-check=0'); + req.rawBody = ''; + + req.setEncoding('utf8'); + + req.on('data', (chunk) => { + req.rawBody += chunk; + }); + + req.on('end', () => { + next(); + }); + }); + + app.use(bodyParser.raw()); + + if (projectType === 'web') { + const livereload = new Livereload(this.sqz); + livereload.init(app); + } + + const watcher = new Watcher(this.sqz); + watcher.init(); + + app.use('/.build', express.static(`${project.buildPath}`, { fallthrough : false, maxAge : 0 })); + + app.all('*', (req, res) => { + // const data = this.find(req.url, req.method); + this.find(req.url, req.method).then((data) => { + if (!data) { + res.status(400).send({ + error : 'Invalid URL path : There is no any event function configured with this url path' + }); + } else { + const target = `${projectPath}/.build/development/microservices/${data.microservice.identifier}`; + + if (!fs.existsSync(target)) { + this.sqz.cli.log.error(`Microservice ${colors.blue.bold(data.microservice.name)} is not compiled !`); + } + + // callback.call(req, res, data); + process.emit('serveEvent', { + req : req, + res : res, + data : data + }); + } + }); + }); + + const httpServer = () => { + app.listen(port, () => { + const appHost = `localhost:${port}`; + const appUrl = `http://localhost:${port}`; + process.env.APP_HOST = appHost; + process.env.APP_URL = appUrl; + this.sqz.cli.log.info(`Listening on ${colors.blue.bold(appUrl)}`); + resolve(); + }).on('error', (err) => { + if (err.code === 'EADDRINUSE') { + port += 1; + httpServer(); + } + }); + }; + + httpServer(); + }); + } + + /** + * Find a function's HTTP event that matches to the current requested URL + * + * @param url - http request url + * @param method - http method + */ + find(url, method) { + return new Promise((resolve) => { + this.sqz.lifecycle.run(['microservices:load']).then(() => { + let data = null; + + _.forEach(this.sqz.vars.microservices, (microservice) => { + _.forEach(microservice.functions, (func, name) => { + func.name = name; + _.forEach(func.events, (val) => { + const type = Object.keys(val)[0]; + const event = val[type]; + + if (type === 'http') { + const expressPathFormatted = `${event.path.replace(/{(.*?)}/g, ':$1')}`; + const pattern = new UrlPattern(expressPathFormatted); + const patternMatch = pattern.match(url.split('?')[0]); + if (patternMatch && event.method.toUpperCase() === method) { + data = { + microservice : microservice, + func : func, + event : event, + pathParameters : patternMatch + }; + } + } + }); + }); + }); + + resolve(data); + }); + }); + } +} + +module.exports = Express; diff --git a/lib/livereload.js b/lib/livereload.js new file mode 100644 index 0000000..72f1a41 --- /dev/null +++ b/lib/livereload.js @@ -0,0 +1,29 @@ +'use strict'; + +const livereload = require('livereload'); +const livereloadConnect = require('connect-livereload'); +const path = require('path'); + +/** + * Class that serves a Squeezer project + */ +class Livereload { + constructor(sqz) { + this.sqz = sqz; + } + + init(app) { + const projectPath = this.sqz.vars.project.path; + const conf = this.sqz.yaml.parse(`${projectPath}/livereload.yml`, { projectPath : projectPath }); + const serverConf = conf.server; + const watchConf = conf.watch.map(val => path.normalize(val)); + + const server = livereload.createServer(serverConf); + server.watch(watchConf); + app.use(livereloadConnect({ + port : 35729 + })); + } +} + +module.exports = Livereload; diff --git a/lib/watcher.js b/lib/watcher.js new file mode 100644 index 0000000..845ca00 --- /dev/null +++ b/lib/watcher.js @@ -0,0 +1,204 @@ +'use strict'; + +const path = require('path'); +const _ = require('lodash'); +const paperwork = require('precinct').paperwork; +const chokidar = require('chokidar'); +const Promise = require('bluebird'); +const fs = require('fs'); +const walkSync = require('walk-sync'); + +/** + * Class that serves a Squeezer project + */ +class Watcher { + constructor(sqz) { + this.sqz = sqz; + this.projectType = this.sqz.vars.project.type; + const projectPath = this.sqz.vars.project.path; + this.globs = this.sqz.yaml.parse(`${projectPath}/watcher.yml`, { projectPath : projectPath }).globs; + } + + /** + * initialize the file watcher + */ + init() { + const globs = _.union(this.globs.include, this.globs.ignore.map(val => `!${val}`)); + const watcher = chokidar.watch(globs, { persistent : true }); + let watcherReady = false; + + watcher + .on('error', error => this.sqz.cli.log.error(error)) + .on('ready', () => { + watcherReady = true; + }) + .on('all', (event, src) => { + // console.log(event, src); + // if (watcherReady && _.includes(['add', 'change'], event)) { + if (watcherReady) { + this.compile(src); + } + }); + } + + compile(src) { + const tree = this.buildCompileTree(); + _.forEach(tree, (obj) => { + if (obj.srcPath === src) { + const compileCmds = this.sqz.yaml.parse(obj.compilePath, obj.options); + Promise.each(Object.keys(compileCmds), (key) => { + const command = compileCmds[key]; + return this.sqz.command.run(command.description, command.bin, command.args || [], true); + }); + } + }); + } + + buildCompileTree() { + const tree = []; + const projectPath = this.sqz.vars.project.path; + + const addPath = (srcPath, compilePath, options) => { + let pathExists = false; + _.forEach(tree, (obj) => { + if (obj.srcPath === srcPath) pathExists = true; + }); + if (!pathExists) { + tree.push({ + srcPath : srcPath, + compilePath : compilePath, + options : options + }); + } + }; + + let srcPaths; + let options; + + // add main assets + if (this.projectType === 'web') { + options = { + projectPath : path.normalize(projectPath), + webpackBin : `${projectPath}/node_modules/.bin/webpack`, + webpackConfigPath : `${projectPath}/lib/webpack`, + source : path.normalize(`${projectPath}/web/assets`), + target : path.normalize(`${projectPath}/.build/development/assets/main`), + staticSource : `${projectPath}/web/assets/static`, + staticTarget : `${projectPath}/.build/development/assets/main/static` + }; + + srcPaths = _.union( + this.getFileDeps(`${projectPath}/web/assets/js/main.js`), + walkSync(`${projectPath}/web/assets/js`, { directories : false }) + .map(val => `${projectPath}/web/assets/js/${val}`) + ); + _.forEach(srcPaths, (src) => { + addPath(src, `${projectPath}/lib/hooks/commands/compile/development/main.js.assets.yml`, options); + }); + + + srcPaths = _.union( + this.getFileDeps(`${projectPath}/web/assets/sass/main.scss`), + walkSync(`${projectPath}/web/assets/sass`, { directories : false }) + .map(val => `${projectPath}/web/assets/sass/${val}`) + ); + _.forEach(srcPaths, (src) => { + addPath(src, `${projectPath}/lib/hooks/commands/compile/development/sass.assets.yml`, options); + }); + + srcPaths = walkSync(`${projectPath}/web/assets/static`, { directories : false }); + _.forEach(srcPaths, (src) => { + addPath(`${projectPath}/web/assets/static/${src}`, `${projectPath}/lib/hooks/commands/compile/development/static.assets.yml`, options); + }); + } + + // add microservices sources and assets + const microservices = this.sqz.vars.microservices; + _.forEach(microservices, (microservice) => { + options = { + projectPath : path.normalize(projectPath), + webpackBin : `${projectPath}/node_modules/.bin/webpack`, + webpackConfigPath : `${projectPath}/lib/webpack`, + source : path.normalize(`${microservice.path}/src/web/assets`), + target : `${projectPath}/.build/development/assets/microservices/${microservice.identifier}`, + staticSource : `${microservice.path}/src/web/assets/static`, + staticTarget : `${projectPath}/.build/development/assets/microservices/${microservice.identifier}/static` + }; + + // add assets + if (this.projectType === 'web') { + srcPaths = _.union( + this.getFileDeps(`${microservice.path}/src/web/assets/js/main.js`), + walkSync(`${microservice.path}/src/web/assets/js`, { directories : false }) + .map(val => `${microservice.path}/src/web/assets/js/${val}`) + ); + _.forEach(srcPaths, (src) => { + addPath(src, `${projectPath}/lib/hooks/commands/compile/development/js.assets.yml`, options); + }); + + srcPaths = _.union( + this.getFileDeps(`${microservice.path}/src/web/assets/sass/main.scss`), + walkSync(`${microservice.path}/src/web/assets/sass`, { directories : false }) + .map(val => `${microservice.path}/src/web/assets/${val}`) + ); + _.forEach(srcPaths, (src) => { + addPath(src, `${projectPath}/lib/hooks/commands/compile/development/sass.assets.yml`, options); + }); + + srcPaths = walkSync(`${microservice.path}/src/web/assets/static`, { directories : false }); + _.forEach(srcPaths, (src) => { + addPath(`${microservice.path}/src/web/assets/static/${src}`, `${projectPath}/lib/hooks/commands/compile/development/static.assets.yml`, options); + }); + } + + // add microservice's sources + options = { + project : this.sqz.vars.project, + microservice : microservice, + source : `${microservice.path}/src`, + output : `${projectPath}/.build/development/microservices/${microservice.identifier}` + }; + + srcPaths = walkSync(`${microservice.path}/src`, { + globs : ['**/*'], + ignore : _.concat(this.globs.ignore, '**/web/assets/*'), + directories : false + }).map(val => `${microservice.path}/src/${val}`); + _.forEach(srcPaths, (src) => { + addPath(src, `${projectPath}/lib/hooks/commands/compile/development/microservice.yml`, options); + }); + }); + + return tree; + } + + /** + * Get deep file inclusions for a specific file + * + * @param file + * @returns {Array} + */ + getFileDeps(file) { + const allDeps = []; + allDeps.push(path.resolve(file)); + + const check = (checkFile) => { + const filePath = path.parse(checkFile); + const deps = paperwork(checkFile) + .filter(val => val.match(/^[.\\|..\\]/)) + .map(val => path.resolve(`${filePath.dir}`, `${val}${filePath.ext}`)); + + deps.forEach((dep) => { + if (!_.includes(allDeps, dep) && fs.existsSync(dep)) { + allDeps.push(dep); + check(dep); + } + }); + }; + check(file); + + return allDeps; + } +} + +module.exports = Watcher; diff --git a/package.json b/package.json new file mode 100644 index 0000000..3dfae7c --- /dev/null +++ b/package.json @@ -0,0 +1,78 @@ +{ + "name": "squeezer-serve", + "version": "0.0.1", + "description": "Squeezer Serve Plugin . This plugin enables serving support for local development within the Squeezer Framework.", + "homepage": "https://squeezer.io/", + "bin": {}, + "keywords": [ + "squeezer.io", + "squeezer.io framework", + "squeezer framework", + "squeezer", + "squeezer cli", + "squeezer generator", + "serverless", + "squeezer serve", + "serverless framework", + "microservices", + "micro", + "service", + "microservices", + "aws lambda", + "lambda", + "API", + "web", + "web app", + "swagger", + "swagger api docs", + "pug", + "reactjs", + "jade", + "aws", + "amazon web services lambda", + "api gateway", + "cloudfront", + "azure functions", + "google cloud functions", + "ibm openwhisk", + "iot", + "internet of things" + ], + "repository": { + "type": "git", + "url": "https://github.com/SqueezerIO/squeezer-azure" + }, + "scripts": { + "eslint": "eslint . --cache", + "jsinspect": "jsinspect -t 50 ./" + }, + "contributors": [ + { + "name": "Nick Chisiu", + "email": "nick@squeezer.io", + "web": "https://Squeezer.IO/" + } + ], + "license": "MIT", + "dependencies": { + "bluebird": "^3.5.0", + "body-parser": "^1.17.2", + "chokidar": "^1.7.0", + "colors": "^1.1.2", + "connect-livereload": "^0.6.0", + "express": "^4.15.4", + "livereload": "^0.6.2", + "precinct": "^3.6.0", + "url-pattern": "^1.0.3", + "walk-sync": "^0.3.2" + }, + "devDependencies": { + "eslint": "^3.19.0", + "eslint-config-airbnb": "^14.0.0", + "eslint-config-airbnb-base": "^11.0.1", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jsx-a11y": "^3.0.2", + "eslint-plugin-react": "^6.10.0", + "jsinspect": "^0.12.7" + } +}