28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/examples/isomorphic-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "isomorphic-react-example",
3 | "version": "1.2.0",
4 | "description": "Example of an isomorphic app using Isomorphine, React and Express",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "gulp start",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "keywords": [
11 | "isomorphine",
12 | "example"
13 | ],
14 | "author": "David Oliveros",
15 | "license": "MIT",
16 | "dependencies": {
17 | "babel": "^5.8.3",
18 | "express": "^4.13.1",
19 | "isomorphine": "0.6.0",
20 | "react": "^0.13.3"
21 | },
22 | "devDependencies": {
23 | "babel-core": "^5.8.9",
24 | "babel-eslint": "^4.0.5",
25 | "babel-loader": "^5.3.2",
26 | "eslint": "^0.24.1",
27 | "eslint-plugin-react": "^3.0.0",
28 | "gulp": "^3.9.0",
29 | "gulp-task-loader": "^1.2.1",
30 | "gulp-nodemon": "^2.0.4",
31 | "webpack": "^1.10.5",
32 | "webpack-dev-server": "^1.10.1"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/test/webpack.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var path = require('path');
3 | var loader = require('../src/webpack-loader');
4 | var expect = require('chai').expect;
5 |
6 | describe('Webpack Loader', function() {
7 | var source;
8 | var sourcePath = path.resolve(__dirname, './mocks/index.js');
9 |
10 | var webpackContext = {
11 | context: path.dirname(sourcePath)
12 | };
13 |
14 | before(function(done) {
15 | fs.readFile(sourcePath, { encoding: 'utf8' }, function(err, file) {
16 | if (err) return done(err);
17 | source = file;
18 | done();
19 | });
20 | });
21 |
22 | it('should correctly transform a file', function() {
23 | expect(source.indexOf('isomorphine.proxy()')).to.be.gt(-1);
24 |
25 | var result = loader.call(webpackContext, source);
26 |
27 | expect(result).to.be.a('string');
28 | expect(result.indexOf('isomorphine.proxy()')).to.be.lt(0);
29 | expect(result.indexOf('isomorphine.proxy(__entityMap)')).to.be.gt(-1);
30 | expect(result.indexOf('function __isomorphicAPIFactory() {')).to.be.gt(-1);
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 David Oliveros
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "isomorphine",
3 | "version": "0.10.2",
4 | "description": "Access server-side modules from the browser, remotely.",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "node node_modules/.bin/mocha"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/d-oliveros/isomorphine.git"
12 | },
13 | "keywords": [
14 | "rpc",
15 | "webpack",
16 | "loader",
17 | "remote",
18 | "proxy",
19 | "isomorphic",
20 | "morphic",
21 | "model"
22 | ],
23 | "author": "David Oliveros",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/d-oliveros/isomorphine/issues"
27 | },
28 | "homepage": "https://github.com/d-oliveros/isomorphine#readme",
29 | "browser": {
30 | "./index.js": "./browser.js"
31 | },
32 | "dependencies": {
33 | "body-parser": "^1.13.3",
34 | "debug": "^2.2.0",
35 | "es6-promise": "^3.0.2",
36 | "express": "^4.13.3",
37 | "http-errors": "^1.3.1",
38 | "superagent": "^2.0.0"
39 | },
40 | "devDependencies": {
41 | "chai": "^3.2.0",
42 | "es6-promise": "^3.0.2",
43 | "mocha": "^2.2.5",
44 | "supertest": "^1.0.1"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/examples/barebone/src/client.js:
--------------------------------------------------------------------------------
1 | var api = require('./api');
2 | var User = api.User;
3 | var Post = api.Post;
4 |
5 | // Isomorphine is framework-agnostic, so you can use any framework you want
6 | // React, Angular, Backbone, Ember, etc...
7 |
8 | /**
9 | * Loads or creates a user, when clicking the load user button.
10 | */
11 | $('#load-user').on('click', function() {
12 | var username = $('#username').val();
13 |
14 | if (!username) {
15 | return alert('Please specify a username to load');
16 | }
17 |
18 | log('Calling isomorphic method: User.load');
19 | User.load({ name: username }, { select: ['name'] }, function(err, user) {
20 | if (err) return console.error(err);
21 |
22 | if (user) {
23 | return log('User found: ' + user.name);
24 | }
25 |
26 | log('User "' + username + '" not found. Creating a new user');
27 | log('Calling isomorphic method: User.create');
28 |
29 | User.create({ name: username }, function(err, user) {
30 | if (err) return console.error(err);
31 |
32 | log('User created: ' + user.name);
33 | });
34 | });
35 | });
36 |
37 | /**
38 | * Creates a new post, when clicking the create post button.
39 | */
40 | $('#create-post').on('click', function() {
41 | var title = $('#post-title').val();
42 |
43 | if (!title) {
44 | return alert('Please specify the post\'s title');
45 | }
46 |
47 | log('Calling isomorphic method: Post.create');
48 | Post.create(title, function(err, post) {
49 | if (err) return console.error(err);
50 | log('Created post ' + post._id + ' - "' + post.title + '"');
51 | });
52 | });
53 |
54 | function log(msg) {
55 | console.log(msg);
56 | $('#console').append(msg + '\n');
57 | }
58 |
--------------------------------------------------------------------------------
/examples/isomorphic-react/client/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { User, Post } from '../api';
3 |
4 | export default class App extends React.Component {
5 | constructor(props) {
6 | super(props);
7 |
8 | this.state = props.state || {
9 | user: null,
10 | post: null
11 | };
12 | }
13 |
14 | componentDidMount() {
15 | this.createPost();
16 | this.loadUser();
17 | }
18 |
19 | createPost() {
20 | let newPost = {
21 | title: `Morphine #${Date.now()}`,
22 | body: 'This post was created from the browser.'
23 | };
24 |
25 | console.log(`Calling isomorphic method: Post.create`);
26 |
27 | Post.create(newPost, (err, post) => {
28 | if (err) return console.error(err);
29 | this.setState({ post });
30 | });
31 | }
32 |
33 | loadUser() {
34 | console.log('Calling isomorphic method: User.load');
35 |
36 | User.load({ _id: 3 }, { select: ['name'] }, (err, user) => {
37 | if (err) return console.error(err);
38 |
39 | if (user) {
40 | console.log(`User found: ${user.name}`);
41 | return this.setState({ user });
42 | }
43 |
44 | else {
45 | console.log(`User not found. Creating a new user.`);
46 | console.log('Calling isomorphic method: User.create');
47 |
48 | User.create({ _id: 3, name: 'somename' }, (err, user) => {
49 | if (err) return console.error(err);
50 |
51 | console.log(`User found: ${user.name}`);
52 | this.setState({ user });
53 | });
54 | }
55 | });
56 | }
57 |
58 | render() {
59 | let {user, post} = this.state;
60 |
61 | return (
62 |
63 |
Isomorphine example.
64 |
65 | { !user ? null :
User is loaded.
}
66 | { !post
67 | ?
No post loaded yet.
68 | : (
69 |
70 |
{ post.title }
71 |
{ post.body }
72 |
73 | )
74 | }
75 |
76 | );
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/examples/isomorphic-react/server/models.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file represents the database models.
3 | * You can use any database and any library you want, because the browser
4 | * never gets here. The browser, instead of requiring the database, is
5 | * proxying the remote procedure calls
6 | *
7 | * This is to demostrate that you can require browser-incompatible modules
8 | * in the API endpoints, without breaking the browser or bloating
9 | * the bundled file.
10 | */
11 | import net from 'net'; // eslint-disable-line
12 | import fs from 'fs'; // eslint-disable-line
13 |
14 | // Eg.
15 | // import mongoose from 'mongoose';
16 | // export default Model = mongoose.model('Model', modelSchema);
17 |
18 | let state = {
19 | posts: {},
20 | users: {}
21 | };
22 |
23 | let models = {
24 | Post: {
25 | create(newPost, callback) {
26 | newPost.created = Date.now();
27 | state.posts[newPost.title] = newPost;
28 |
29 | console.log(`Post ${newPost.title} created.`);
30 |
31 | callback(null, newPost);
32 | }
33 | },
34 |
35 | User: {
36 | load(query, options, callback) {
37 | if (!state.users[query._id]) return callback();
38 |
39 | let user = state.users[query._id];
40 | let ret = {};
41 |
42 | options.select.forEach((select) => {
43 | ret[select] = user[select];
44 | });
45 |
46 | console.log(`Loaded user: ${user._id}`);
47 |
48 | callback(null, ret);
49 | },
50 |
51 | create(user, callback) {
52 | user.created = Date.now();
53 | state.users[user._id] = user;
54 |
55 | console.log(`User ${user._id} created.`);
56 |
57 | callback(null, user);
58 | }
59 | }
60 | };
61 |
62 | /**
63 | * Adds latency to every async callback method in each model.
64 | */
65 | for (let entity in models) {
66 | for (let method in models[entity]) {
67 | models[entity][method] = fakeLatency(models[entity][method]);
68 | }
69 | }
70 |
71 | function fakeLatency(cb) {
72 | return function() {
73 | let args = Array.prototype.slice.call(arguments);
74 | setTimeout(() => {
75 | cb.apply(this, args);
76 | }, Math.floor(Math.random() * 1400));
77 | };
78 | }
79 |
80 | export default models;
81 |
--------------------------------------------------------------------------------
/examples/barebone/src/db.js:
--------------------------------------------------------------------------------
1 |
2 | // This file represents the database models.
3 | // I mocked up some dummy models for demostration purposes only.
4 |
5 | /**
6 | * You can use any database and any library you want, because the browser
7 | * never gets here. The browser, instead of requiring the database, is
8 | * proxying remote procedure calls to the server.
9 | *
10 | * This is to demostrate that you can require browser-incompatible modules
11 | * in the API endpoints, without breaking the browser or bloating
12 | * the bundled file.
13 | */
14 | var net = require('net'); // eslint-disable-line
15 | var fs = require('fs'); // eslint-disable-line
16 |
17 | // Eg.
18 | // var mongoose = require('mongoose');
19 | // module.exports = mongoose.model('Model', modelSchema); etc...
20 |
21 | var databaseState = {
22 | posts: {},
23 | users: {}
24 | };
25 |
26 | // Dummy models...
27 | var models = {
28 | Post: {
29 | create: function(postTitle, callback) {
30 | var newPost = {
31 | _id: Math.floor(Math.random() * 1000),
32 | title: postTitle,
33 | created: Date.now()
34 | };
35 |
36 | databaseState.posts[newPost._id] = newPost;
37 |
38 | console.log('Post "' + newPost.title + '" created');
39 |
40 | callback(null, newPost);
41 | }
42 | },
43 |
44 | User: {
45 | load: function(query, options, callback) {
46 | if (!databaseState.users[query.name]) return callback();
47 |
48 | var user = databaseState.users[query.name];
49 | var ret = {};
50 |
51 | options.select.forEach(function(select) {
52 | ret[select] = user[select];
53 | });
54 |
55 | console.log('Loaded user: ' + user.name);
56 |
57 | callback(null, ret);
58 | },
59 |
60 | create: function(user, callback) {
61 | user.created = Date.now();
62 | databaseState.users[user.name] = user;
63 |
64 | console.log('User "' + user.name + '" created.');
65 |
66 | callback(null, user);
67 | }
68 | }
69 | };
70 |
71 | /**
72 | * Adds latency to every async callback method in each model.
73 | */
74 | for (var entity in models) {
75 | for (var method in models[entity]) {
76 | models[entity][method] = fakeLatency(models[entity][method]);
77 | }
78 | }
79 |
80 | function fakeLatency(cb) {
81 | return function() {
82 | var args = Array.prototype.slice.call(arguments);
83 | var delay = 50;
84 | var self = this;
85 |
86 | setTimeout(function() {
87 | cb.apply(self, args);
88 | }, delay);
89 | };
90 | }
91 |
92 | module.exports = models;
93 |
--------------------------------------------------------------------------------
/src/server/router.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var bodyParser = require('body-parser');
3 | var createError = require('http-errors');
4 | var debug = require('debug')('isomorphine:router');
5 | var isObject = require('../util').isObject;
6 | var isES6Function = require('../util').isES6Function;
7 | var getES6Function = require('../util').getES6Function;
8 | var ctrls = require('./controllers');
9 |
10 | var getPayload = ctrls.getPayload;
11 | var callEntityMethod = ctrls.callEntityMethod;
12 | var serve = ctrls.serve;
13 |
14 | /**
15 | * Creates an express or connect-styled router,
16 | * and exposes the provided modules in the router's API surface area.
17 | *
18 | * @param {Object} modules The entities to use.
19 | * @returns {Function} An express app instance.
20 | *
21 | * @providesModule router
22 | */
23 | function createRouter(modules) {
24 | debug('Creating a new router. Modules: ' + JSON.stringify(modules, null, 3));
25 |
26 | var router = express();
27 | router.use(bodyParser.json());
28 |
29 | // Map the requested entity path with the actual serverside entity
30 | router.use('/isomorphine', methodLoader(modules));
31 |
32 | // Proxy request pipeline
33 | router.post('/isomorphine/*', getPayload, callEntityMethod, serve);
34 |
35 | return router;
36 | }
37 |
38 | /**
39 | * Maps the parameter "entity" to the actual server-side entity.
40 | *
41 | * @param {Object} modules The entities available in this api.
42 | * @returns {Function} Middleware function that maps the :entity param
43 | * with the real entity, and exposes it in req.
44 | */
45 | function methodLoader(modules) {
46 | return function(req, res, next) {
47 | if (req.path === '/') return next();
48 |
49 | var path = req.path.substr(1).split('/');
50 | var currModule = modules;
51 | var isLastIndex, p, method;
52 |
53 | debug('Looking for isomorphine entity in: ' + path.join('.'));
54 |
55 | for (var i = 0, len = path.length; i < len; i++) {
56 | isLastIndex = i === (len - 1);
57 | p = path[i];
58 |
59 | // Expect a function when last index
60 | if (isLastIndex && isES6Function(currModule[p])) {
61 | method = getES6Function(currModule[p]);
62 | }
63 |
64 | // Expect an object when not last index
65 | else if (!isLastIndex && isObject(currModule[p])) {
66 | currModule = currModule[p];
67 | }
68 |
69 | // Return a 404 if the entity was not found
70 | else {
71 | return next(createError(404, 'No method found at this path'));
72 | }
73 | }
74 |
75 | // Reference the serverside method in req
76 | debug('Entity found');
77 | req.serversideMethod = method;
78 |
79 | next();
80 | };
81 | }
82 |
83 | module.exports = createRouter;
84 |
--------------------------------------------------------------------------------
/src/server/controllers.js:
--------------------------------------------------------------------------------
1 | var util = require('../util');
2 | var debug = require('debug')('isomorphine:controllers');
3 |
4 | /**
5 | * @providesModule controllers
6 | */
7 | exports.getPayload = getPayload;
8 | exports.callEntityMethod = callEntityMethod;
9 | exports.serve = serve;
10 |
11 | /**
12 | * Processes the client-side payload, and transforms the client-side
13 | * callback function signal to an actual callback function
14 | */
15 | function getPayload(req, res, next) {
16 | req.hasCallback = false;
17 | req.payload = req.body.payload || [];
18 |
19 | // Determines if the request is asynchronous or not
20 | req.payload.forEach(function(arg, i) {
21 | if (arg === '__clientCallback__') {
22 | req.hasCallback = true;
23 | req.clientCallbackIndex = i;
24 | }
25 | });
26 |
27 | debug('Got payload' + (req.hasCallback ? ' with callback' : '') + ': ' +
28 | JSON.stringify(req.payload, null, 3));
29 |
30 | next();
31 | }
32 |
33 | /**
34 | * Calls the server-side entity, and returns the results to the client
35 | */
36 | function callEntityMethod(req, res, next) {
37 | var payload = req.payload;
38 | var method = req.serversideMethod;
39 |
40 | if (req.hasCallback) {
41 |
42 | debug('Transforming callback function');
43 | payload[req.clientCallbackIndex] = function(err) {
44 | if (err) {
45 | return next(err);
46 | }
47 |
48 | var values = Array.prototype.slice.call(arguments).slice(1);
49 |
50 | debug('Callback function called. Values are:', values);
51 |
52 | res.entityResponse = values;
53 |
54 | next();
55 | };
56 | }
57 |
58 | debug('Calling ' + req.path + ' with arguments:', payload);
59 |
60 | var context = {
61 | req: req,
62 | xhr: true,
63 | setCookie: res.cookie.bind(res),
64 | clearCookie: res.clearCookie.bind(res)
65 | };
66 |
67 | var ret;
68 |
69 | // Calls the requested serverside method.
70 | // Applies the payload, and provides a context for validation purposes.
71 | // Caches errors in the method's scope, and sends it to the next error handler.
72 | try {
73 | ret = method.apply(context, payload);
74 | } catch(err) {
75 | return next(err);
76 | }
77 |
78 | if (util.isPromise(ret)) {
79 | ret.then(function(resolved) {
80 | res.entityResponse = [resolved];
81 | next();
82 | })
83 | .catch(function(err) {
84 | next(err);
85 | });
86 | }
87 |
88 | // If the request is not expecting the response from the entity,
89 | // send a generic 'Ok' response.
90 | else if (!req.hasCallback) {
91 | res.entityResponse = [ret];
92 | debug('Not asynchronous. Returning value: ', res.entityResponse);
93 | next();
94 | }
95 | }
96 |
97 | /**
98 | * Serves the value in req.entityResponse as a JSON object.
99 | */
100 | function serve(req, res) {
101 | var responseIsArray = Array.isArray(res.entityResponse);
102 | util.invariant(responseIsArray, 'Response values are required.');
103 |
104 | res.json({ values: res.entityResponse });
105 | }
106 |
--------------------------------------------------------------------------------
/src/webpack-loader.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var path = require('path');
3 |
4 | /**
5 | * Webpack loader for isomorphine.
6 | * @type {webpack.Loader}
7 | *
8 | * Transforms isomorphine.proxy({String}) to isomorphine.proxy({Object}).
9 | * It generates an entity map from the provided path passed to isomorphine.proxy
10 | * without requiring its modules.
11 | *
12 | * It doesn't require the module itself to prevent serverside entities
13 | * to be bundled together, thus avoiding leaking sensitive data
14 | * and non-browser-compatible libraries.
15 | *
16 | * @providesModule webpack-loader
17 | *
18 | * @param {String} source The file's source code.
19 | * @return {String} The transformed file's source code.
20 | */
21 | module.exports = function webpackLoader(source) {
22 | if (this.cacheable) {
23 | this.cacheable();
24 | }
25 |
26 | if (source.indexOf('isomorphine.proxy(') < 0) {
27 | return source;
28 | }
29 |
30 | var rootdir = getRootdir(this.context, source);
31 | var map = getModuleMapSync(rootdir);
32 |
33 | source = source.replace(/isomorphine\.proxy\(.*\)/, '__isomorphicAPIFactory()');
34 |
35 | source += '\n' + (
36 | 'function __isomorphicAPIFactory() {\n' +
37 | ' var __entityMap = ' + JSON.stringify(map) + ';\n' +
38 | ' return isomorphine.proxy(__entityMap);\n' +
39 | '}'
40 | );
41 |
42 | return source;
43 | };
44 |
45 | /**
46 | * Gets the absolute path to the rootdir argument of the isomorphic.proxy() call.
47 | *
48 | * @param {String} context The absolute path to the file's directory.
49 | * @param {String} source The file's source code.
50 | *
51 | * @return {String} The absolute path, as sent to isomorphine.
52 | */
53 | function getRootdir(context, source) {
54 | var regexMatch = source.match(/isomorphine.proxy\((.*)\)/);
55 | var target = '';
56 |
57 | if (regexMatch) {
58 | target = regexMatch[1];
59 |
60 | if (target[0] === '\'' || target[0] === '"') {
61 | target = target.substr(0, target.length - 1).substr(1);
62 | }
63 |
64 | if (target === '__dirname') {
65 | target = context;
66 | }
67 |
68 | }
69 |
70 | return path.resolve(context, target);
71 | }
72 |
73 | /**
74 | * Maps a directory and generates the module interface map synchronously.
75 | * @todo Make this asynchronous.
76 | *
77 | * @param {dir} dir Absolute path to the directory.
78 | * @return {Object} Module interface map.
79 | */
80 | function getModuleMapSync(dir) {
81 | var map = {};
82 |
83 | fs
84 | .readdirSync(dir)
85 | .filter(function(filename) {
86 | return filename !== 'index.js';
87 | })
88 | .forEach(function(filename) {
89 | var filePath = path.join(dir, filename);
90 | var Stats = fs.lstatSync(filePath);
91 | var isLink = Stats.isSymbolicLink();
92 | var isDir = Stats.isDirectory();
93 | var isFile = Stats.isFile();
94 | var isJS = filename.indexOf('.js') > -1;
95 |
96 | if (!isLink && isDir) {
97 | map[filename] = getModuleMapSync(filePath);
98 | }
99 |
100 | else if (!isLink && isFile && isJS) {
101 | var entityName = filename.replace('.js', '');
102 | map[entityName] = true;
103 | }
104 | });
105 |
106 | return map;
107 | }
108 |
--------------------------------------------------------------------------------
/src/server/factory.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var path = require('path');
3 | var createRouter = require('./router');
4 | var util = require('../util');
5 |
6 | var invariant = util.invariant;
7 | var emptyFunction = util.emptyFunction;
8 | var isObject = util.isObject;
9 | var isFunction = util.isFunction;
10 |
11 | /**
12 | * Creates an isomorphine endpoint router with entities loaded from 'baseDir'.
13 | *
14 | * @warn
15 | * Isomorphine determines your entities' methods by scanning the file structure
16 | * of the base directory. Every folder represents an entity,
17 | * whereas every file in each folder represents an API endpoint, or "route".
18 | *
19 | * @see /lib/server/router.js
20 | *
21 | * @providesModule serverFactory
22 | *
23 | * @param {String} baseDir Path to folder to require from.
24 | * @return {Object} Required modules.
25 | */
26 | module.exports = function routerFactory(baseDir) {
27 | var morphine = requireMethods(baseDir);
28 |
29 | invariant(!morphine.hasOwnProperty('router'),
30 | 'You can\'t use an entity with the name "router"');
31 |
32 | // Create the API endpoint that will listen for RPCs.
33 | morphine.router = createRouter(morphine);
34 |
35 | // Mocks the `morphine.config` method. The method does nothing in the server.
36 | // It is only used in the browser.
37 | morphine.config = emptyFunction;
38 | morphine.addErrorHandler = emptyFunction;
39 | morphine.removeErrorHandler = emptyFunction;
40 |
41 | return morphine;
42 | };
43 |
44 | /**
45 | * Recursively requires the modules in current dir.
46 | *
47 | * @param {String} dir The base directory to require entities from.
48 | * @return {Object} An object with all the modules loaded.
49 | */
50 | function requireMethods(dir) {
51 | if (!dir) dir = getCallerDirname();
52 |
53 | var modules = {};
54 |
55 | fs
56 | .readdirSync(dir)
57 | .filter(function(filename) {
58 | return filename !== 'index.js';
59 | })
60 | .forEach(function(filename) {
61 | var filePath = path.join(dir, filename);
62 | var Stats = fs.lstatSync(filePath);
63 | var isLink = Stats.isSymbolicLink();
64 | var isDir = Stats.isDirectory();
65 | var isFile = Stats.isFile();
66 | var isJS = filename.indexOf('.js') > -1;
67 |
68 | if (!isLink && isDir) {
69 | modules[filename] = requireMethods(filePath);
70 | }
71 |
72 | else if (!isLink && isFile && isJS) {
73 | var entityName = filename.replace('.js', '');
74 | modules[entityName] = require(filePath);
75 |
76 | var hasES6Default = isObject(modules[entityName])
77 | && isFunction(modules[entityName].default);
78 |
79 | if (hasES6Default) {
80 | modules[entityName] = modules[entityName].default;
81 | }
82 | }
83 | });
84 |
85 | return modules;
86 | }
87 |
88 | /**
89 | * Gets the dirname of the caller function that is calling this method.
90 | * @return {String} Absolute path to the caller's directory.
91 | */
92 | function getCallerDirname() {
93 | var orig = Error.prepareStackTrace;
94 | Error.prepareStackTrace = function(_, stack){ return stack; };
95 | var err = new Error();
96 | Error.captureStackTrace(err, arguments.callee);
97 | var stack = err.stack;
98 | Error.prepareStackTrace = orig;
99 | var requester = stack[2].getFileName();
100 |
101 | return path.dirname(requester);
102 | }
103 |
--------------------------------------------------------------------------------
/src/client/factory.js:
--------------------------------------------------------------------------------
1 | var changeConfig = require('../util').changeConfig;
2 | var invariant = require('../util').invariant;
3 | var isObject = require('../util').isObject;
4 | var isBoolean = require('../util').isBoolean;
5 | var emptyFunction = require('../util').emptyFunction;
6 | var createProxiedMethod = require('./createProxiedMethod');
7 | var debug = require('debug')('isomorphine:injector');
8 |
9 | /**
10 | * Transforms the API surface area of the serverside modules
11 | * to Proxy instances that will transport the function calls to the server.
12 | *
13 | * Isomorphine determines the server methods by recursively
14 | * scanning the file structure of the base directory.
15 | *
16 | * Every file represents a serverside method, and every folder is
17 | * recursively scanned to map the server methods.
18 | *
19 | * @see /lib/client/createProxiedMethod.js
20 | *
21 | * @providesModule clientFactory
22 | *
23 | * @param {Object|String} entityMap
24 | * A map of the entities to proxy, or the absolute path to the entities' base dir.
25 | *
26 | * If a string is provided, Isomorphine's webpack loader should
27 | * generate a map of the entity's file structure automatically for you.
28 | *
29 | * @return {Object} A proxied mirror of the serverside entities.
30 | */
31 | module.exports = function proxyFactory(entityMap) {
32 | invariant(typeof entityMap === 'object', 'Entity map is not an object. '+
33 | '(Hint: Are you sure you are using the webpack loader?)');
34 |
35 | var params = getConfigFromBrowser();
36 | var morphine = createProxies(params, entityMap);
37 |
38 | morphine.config = changeConfig.bind(this, params);
39 |
40 | morphine.addErrorHandler = function(handler) {
41 | params.errorHandlers.push(handler);
42 | };
43 |
44 | morphine.removeErrorHandler = function(handler) {
45 | var index = params.errorHandlers.indexOf(handler);
46 | if (index > -1) {
47 | params.errorHandlers.splice(index, 1);
48 | }
49 | };
50 |
51 | // Mocks the `morphine.router` property.
52 | // This is only used in the server.
53 | morphine.router = {
54 | listen: emptyFunction
55 | };
56 |
57 | debug('Loaded entity mirror proxies in the browser: ', morphine);
58 |
59 | return morphine;
60 | };
61 |
62 | /**
63 | * Creates proxied methods using a provided map. If parentPath is provided,
64 | * it will be used to build the proxied method's endpoint.
65 | * @param {Object} map The entity map to use.
66 | * @param {Array} parentPath The path to the parent entity.
67 | */
68 | function createProxies(config, map, parentPath) {
69 | parentPath = parentPath || [];
70 |
71 | var isBase = parentPath.length === 0;
72 | var proxies = {};
73 | var path;
74 |
75 | for (var key in map) {
76 | if (map.hasOwnProperty(key)) {
77 | if (isObject(map[key])) {
78 | proxies[key] = createProxies(config, map[key], parentPath.concat([key]));
79 | }
80 | else if (isBoolean(map[key])) {
81 | path = parentPath.join('/') + (isBase ? '' : '/') + key;
82 | proxies[key] = createProxiedMethod(config, path);
83 | }
84 | }
85 | }
86 |
87 | return proxies;
88 | }
89 |
90 | /**
91 | * Gets the default configuration based on environmental variables
92 | * @return {Object} Initial config
93 | */
94 | function getConfigFromBrowser() {
95 | var defaultLocation = {
96 | port: '80',
97 | hostname: 'localhost',
98 | protocol: 'http:'
99 | };
100 |
101 | var wLocation = (global.location)
102 | ? global.location
103 | : defaultLocation;
104 |
105 | var location = {
106 | port: wLocation.port,
107 | host: wLocation.protocol + '//' + wLocation.hostname
108 | };
109 |
110 | var config = {
111 | port: location.port,
112 | host: location.host,
113 | errorHandlers: []
114 | };
115 |
116 | return config;
117 | }
118 |
--------------------------------------------------------------------------------
/test/server.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var request = require('supertest');
3 | var express = require('express');
4 | var expect = require('chai').expect;
5 | var isomorphine = require('../index');
6 |
7 | var mocksPath = path.resolve(__dirname, 'mocks');
8 |
9 | describe('Server', function() {
10 | describe('Factory', function() {
11 |
12 | it('should load all the modules in a folder', function() {
13 | var api = isomorphine.proxy(mocksPath);
14 |
15 | // Isomorphine exports
16 | expect(api).to.be.an('object');
17 | expect(api.router).to.be.a('function');
18 |
19 | // Methods that were required by isomorphine
20 | expect(api.aSingleMethod).to.be.an('function');
21 |
22 | // Support for es6 "export default"
23 | expect(api.NestedEntity).to.be.an('object')
24 | .with.property('aMethod').that.is.a('function');
25 |
26 | expect(api.Entity.doSomething()).to.equal('You got it');
27 | });
28 |
29 | it('should load all the modules in a folder without dir param', function() {
30 | var api = isomorphine.proxy();
31 |
32 | expect(api).to.be.a('object');
33 |
34 | expect(api.mocks).to.be.an('object')
35 | .with.property('map').that.is.an('object')
36 | .that.include.keys(['Entity', 'NestedEntity']);
37 |
38 | expect(api.mocks.NestedEntity).to.be.an('object')
39 | .with.property('ChildEntity').that.is.an('object')
40 | .that.include.keys(['childMethod']);
41 | });
42 | });
43 |
44 | describe('Router', function() {
45 | var app;
46 |
47 | before(function() {
48 | var api = isomorphine.proxy(mocksPath);
49 | app = express();
50 |
51 | app.use(api.router);
52 | app.use(function(err, req, res, next) { // eslint-disable-line
53 | res.sendStatus(err.statusCode || err.status || 500);
54 | });
55 | });
56 |
57 | it('should only accept post requests and return 404(express)', function(done) {
58 | request(app)
59 | .get('/isomorphine/Entity/doSomething')
60 | .expect(404)
61 | .end(done);
62 | });
63 |
64 | it('should call a server-side method and return OK', function(done) {
65 | request(app)
66 | .post('/isomorphine/aSingleMethod')
67 | .expect(200)
68 | .expect('Content-Type', /json/)
69 | .expect({ values: [null] })
70 | .end(done);
71 | });
72 |
73 | it('should call a nested server-side method and return OK', function(done) {
74 | request(app)
75 | .post('/isomorphine/NestedEntity/ChildEntity/childMethod')
76 | .expect(200)
77 | .expect('Content-Type', /json/)
78 | .expect({ values: [null] })
79 | .end(done);
80 | });
81 |
82 | it('should call a server-side entity and return nested results', function(done) {
83 | request(app)
84 | .post('/isomorphine/Entity/doSomethingAsync')
85 | .send({ payload: ['oneParam', 'anotherParam', '__clientCallback__'] })
86 | .expect(200)
87 | .expect('Content-Type', /json/)
88 | .expect({ values: ['Sweet', { nested: { thing: ['true', 'dat'] }}]})
89 | .end(done);
90 | });
91 |
92 | it('should call a server-side entity and provide a context', function(done) {
93 | request(app)
94 | .post('/isomorphine/Entity/withContext')
95 | .send({ payload: ['__clientCallback__'] })
96 | .expect(200)
97 | .end(done);
98 | });
99 |
100 | it('should call a server-side entity and pass the validation', function(done) {
101 | request(app)
102 | .post('/isomorphine/Entity/withValidation')
103 | .send({ payload: ['thekey', '__clientCallback__'] })
104 | .expect(200)
105 | .end(done);
106 | });
107 |
108 | it('should call a server-side entity and fail the validation', function(done) {
109 | request(app)
110 | .post('/isomorphine/Entity/withValidation')
111 | .send({ payload: ['incorrectkey', '__clientCallback__'] })
112 | .expect(401)
113 | .end(done);
114 | });
115 | });
116 | });
117 |
--------------------------------------------------------------------------------
/src/client/createProxiedMethod.js:
--------------------------------------------------------------------------------
1 | var request = require('superagent');
2 | var debug = require('debug')('isomorphine:createProxiedMethod');
3 | var util = require('../util');
4 |
5 | /**
6 | * Creates a new proxied method.
7 | * Serializes the parameters sent in the function's call,
8 | * and sends a POST request to isomorphine's endpoint in the server.
9 | *
10 | * @param {Object} params The server's configuration and error handlers.
11 | * @param {Object} path The path to the serverside method to be called.
12 | *
13 | * @return {Function}
14 | * A proxied method that when called, will serialize the parameters
15 | * in the function's call, and send a POST request to isomorphine's
16 | * endpoint in the server.
17 | *
18 | * @providesModule createProxiedMethod
19 | */
20 | module.exports = function createProxiedMethod(params, path) {
21 | debug('Creating a new proxied method with path "' + path + '"');
22 | return proxiedMethod.bind({}, params, path);
23 | };
24 |
25 | /**
26 | * Serializes the parameters sent in the function's call,
27 | * and sends a POST request to isomorphine's endpoint in the server.
28 | *
29 | * @param {Object} params The server's configuration and error handlers.
30 | * @param {Object} path The path to the serverside method to be called.
31 | */
32 | function proxiedMethod(params, path) {
33 |
34 | // Get the arguments that should be passed to the server
35 | var payload = Array.prototype.slice.call(arguments).slice(2);
36 |
37 | // Save the callback function for later use
38 | var callback = util.firstFunction(payload);
39 |
40 | // Transform the callback function in the arguments into a special key
41 | // that will be used in the server to signal the client-side callback call
42 | payload = util.serializeCallback(payload);
43 |
44 | var endpoint = buildEndpoint(params, path);
45 |
46 | if (callback) {
47 | return doRequest(endpoint, payload, params, callback);
48 | } else {
49 | return util.promisify(doRequest)(endpoint, payload, params);
50 | }
51 | }
52 |
53 | /**
54 | * Runs a request to an isomorphine's endpoint with the provided payload.
55 | *
56 | * @param {String} endpoint The endpoint to request.
57 | * @param {Array} payload The arguments to send.
58 | * @param {Object} params The server's configuration and error handlers.
59 | * @param {Function} callback The callback function to call afterwards.
60 | */
61 | function doRequest(endpoint, payload, params, callback) {
62 | debug('Calling API endpoint: ' + endpoint + '.');
63 |
64 | request
65 | .post(endpoint)
66 | .send({ payload: payload })
67 | .set('Accept', 'application/json')
68 | .end(function(err, res) {
69 | if ((!res || !res.body) && !err) {
70 | err = new Error('No response from server. ' +
71 | '(Hint: Have you mounted isomorphine.router() in your app?)');
72 | }
73 |
74 | if (err) {
75 | return handleError(err, params, callback);
76 | }
77 |
78 | var values = res.body.values;
79 |
80 | if (!values || values.constructor !== Array) {
81 | err = new Error('Fetched payload is not an array.');
82 | return handleError(err, params, callback);
83 | }
84 |
85 | debug('Resolving callback with ' + JSON.stringify(values, null, 3));
86 |
87 | // Sets the error argument to null
88 | values.unshift(null);
89 |
90 | callback.apply(this, values);
91 | });
92 | }
93 |
94 | function handleError(err, params, callback) {
95 | util.invariant(typeof params === 'object', 'Params is required');
96 |
97 | debug('API request failed.', err);
98 | if (params.errorHandlers instanceof Array) {
99 | params.errorHandlers.forEach(function(handler) {
100 | handler(err);
101 | });
102 | }
103 |
104 | if (typeof callback === 'function') {
105 | callback(err);
106 | }
107 | }
108 |
109 | /**
110 | * Builds a method's API endpoint.
111 | *
112 | * @param {Object} config The host and port parameters to use.
113 | * @param {String} path The path to the serverside method.
114 | * @return {String} The endpoint to request.
115 | */
116 | function buildEndpoint(config, path) {
117 | var host = config.host;
118 | var port = config.port;
119 |
120 | if (!host) throw new Error('No host is specified in proxied method config');
121 |
122 | var base = host + (port ? ':' + port : '');
123 | var fullpath = '/isomorphine/' + path;
124 | var endpoint = base + fullpath;
125 |
126 | debug('Built endpoint: ' + endpoint);
127 |
128 | return endpoint;
129 | }
130 |
--------------------------------------------------------------------------------
/test/browser.js:
--------------------------------------------------------------------------------
1 | var expect = require('chai').expect;
2 | var path = require('path');
3 | var isomorphine = require('../browser');
4 | var createProxiedMethod = require('../src/client/createProxiedMethod');
5 | var apiFactory = require('../src/server/factory');
6 | var mapMock = require('./mocks/map');
7 |
8 | describe('Browser', function() {
9 | describe('Factory', function() {
10 | it('should map the entity methods to proxy instances', function() {
11 | var api = isomorphine.proxy(mapMock);
12 |
13 | expect(api.index).to.not.be.a('function');
14 | expect(api.aSingleMethod).to.be.a('function');
15 | expect(api.Entity.doSomething).to.be.a('function');
16 | expect(api.NestedEntity.ChildEntity.childMethod).to.be.a('function');
17 | });
18 | });
19 |
20 | describe('Proxied Methods', function() {
21 | it('should proxy an entity method through the rest API', function(done) {
22 |
23 | var config = {
24 | host: 'http://127.0.0.1',
25 | port: 3000
26 | };
27 |
28 | var methodPath = 'Entity/doSomethingAsync';
29 |
30 | // Instanciates a new Proxy
31 | var proxiedMethod = createProxiedMethod(config, methodPath);
32 |
33 | // Creates a new API to listen to the clientside proxied function calls
34 | var api = apiFactory(path.join(__dirname, 'mocks'));
35 |
36 | // Starts the test's API server
37 | var server = api.router.listen(3000, function(err) {
38 | if (err) return done(err);
39 |
40 | proxiedMethod('something', { another: 'thing' }, function(err, firstRes, secondRes) {
41 | if (err) return done(err);
42 | expect(firstRes).to.equal('Sweet');
43 | expect(secondRes).to.deep.equal({ nested: { thing: ['true', 'dat'] }});
44 | server.close(done);
45 | });
46 | });
47 | });
48 |
49 | it('should proxy an entity method with overridden host and port', function(done) {
50 | var clientApi = isomorphine.proxy(mapMock);
51 |
52 | var config = {
53 | host: 'http://127.0.0.1',
54 | port: 6689
55 | };
56 |
57 | clientApi.config(config);
58 |
59 | // Creates a new API to listen to the clientside proxied function calls
60 | // In a real-world example, clientApi and serverApi will be the same code,
61 | // reused in an isomorphic fashion.
62 | var serverApi = apiFactory(path.join(__dirname, 'mocks'));
63 |
64 | // Starts the test's API in port 6689
65 | var server = serverApi.router.listen(6689, function(err) {
66 | if (err) return done(err);
67 |
68 | clientApi.Entity.doSomethingAsync('something', { another: 'thing' }, function(err, firstRes, secondRes) {
69 | if (err) return done(err);
70 | expect(firstRes).to.equal('Sweet');
71 | expect(secondRes).to.deep.equal({ nested: { thing: ['true', 'dat'] }});
72 | server.close(done);
73 | });
74 | });
75 | });
76 |
77 | it('should return a promise if no callback is provided', function(done) {
78 | var methodPath = 'Entity/returnPromise';
79 |
80 | var config = {
81 | host: 'http://127.0.0.1',
82 | port: 3000
83 | };
84 |
85 | // Instanciates a new Proxy
86 | var proxiedMethod = createProxiedMethod(config, methodPath);
87 |
88 | // Creates a new API to listen to the clientside proxied function calls
89 | var api = apiFactory(path.join(__dirname, 'mocks'));
90 |
91 | // Starts the test's API server
92 | var server = api.router.listen(3000, function(err) {
93 | if (err) return done(err);
94 |
95 | proxiedMethod('something', { another: 'thing' })
96 | .then(function(value) {
97 | expect(value).to.equal('Cool');
98 | server.close(done);
99 | })
100 | .catch(done);
101 | });
102 | });
103 |
104 | it('should resolve a value when entity returns a raw value', function(done) {
105 | var methodPath = 'Entity/returnValue';
106 |
107 | var config = {
108 | host: 'http://127.0.0.1',
109 | port: 3000
110 | };
111 |
112 | // Instanciates a new Proxy
113 | var proxiedMethod = createProxiedMethod(config, methodPath);
114 |
115 | // Creates a new API to listen to the clientside proxied function calls
116 | var api = apiFactory(path.join(__dirname, 'mocks'));
117 |
118 | // Starts the test's API server
119 | var server = api.router.listen(3000, function(err) {
120 | if (err) return done(err);
121 |
122 | proxiedMethod()
123 | .then(function(value) {
124 | expect(value).to.equal('Sync value');
125 | server.close(done);
126 | })
127 | .catch(done);
128 | });
129 | });
130 | });
131 | });
132 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | var debug = require('debug')('isomorphine:util');
2 | var Promise = global.Promise || require('es6-promise').Promise;
3 |
4 | /**
5 | * @providesModule util
6 | */
7 | exports.emptyFunction = emptyFunction;
8 | exports.firstFunction = firstFunction;
9 | exports.serializeCallback = serializeCallback;
10 | exports.promisify = promisify;
11 | exports.isObject = isObject;
12 | exports.isBoolean = isBoolean;
13 | exports.isFunction = isFunction;
14 | exports.isES6Function = isES6Function;
15 | exports.getES6Function = getES6Function;
16 | exports.isPromise = isPromise;
17 | exports.changeConfig = changeConfig;
18 | exports.invariant = invariant;
19 |
20 | /**
21 | * An empty function.
22 | */
23 | function emptyFunction() {}
24 |
25 | /**
26 | * Returns the first function in an array.
27 | *
28 | * @param {Array} args The array to take the function from.
29 | * @return {Function} The resulting function, or null.
30 | */
31 | function firstFunction(args) {
32 | for (var i = 0, len = args.length; i < len; i++) {
33 | if (typeof args[i] === 'function') {
34 | return args[i];
35 | }
36 | }
37 |
38 | return null;
39 | }
40 |
41 | /**
42 | * Transforms the client's callback function to a callback notice string.
43 | *
44 | * @param {Array} args Array of arguments to transform.
45 | * @return {Array} The transformed arguments array.
46 | */
47 | function serializeCallback(args) {
48 | var callback;
49 |
50 | debug('Transforming callback in ', args);
51 |
52 | return args.map(function(arg) {
53 | if (typeof arg !== 'function') return arg;
54 |
55 | // It shouldn't be an argument after the callback function
56 | invariant(!callback, 'Only one callback function is allowed.');
57 |
58 | callback = arg;
59 |
60 | return '__clientCallback__';
61 | });
62 | }
63 |
64 | /**
65 | * Transforms a callback-based function flow to a promise-based flow
66 | */
67 | function promisify(func) {
68 | return function promisified() {
69 | var args = Array.prototype.slice.call(arguments);
70 | var context = this;
71 |
72 | return new Promise(function(resolve, reject) {
73 | try {
74 | func.apply(context, args.concat(function(err, data) {
75 | if (err) {
76 | return reject(err);
77 | }
78 |
79 | resolve(data);
80 | }));
81 | } catch(err) {
82 | reject(err);
83 | }
84 | });
85 | };
86 | }
87 |
88 | /**
89 | * Checks if the passed in variable is an object.
90 | * @param {Mixed} obj The variable to check.
91 | * @return {Boolean} True if the variable is an object.
92 | */
93 | function isObject(obj) {
94 | return typeof obj === 'object' && obj !== null;
95 | }
96 |
97 | /**
98 | * Checks if the passed in variable is a boolean.
99 | * @param {Mixed} obj The variable to check.
100 | * @return {Boolean} True if the variable is a Boolean.
101 | */
102 | function isBoolean(obj) {
103 | return typeof obj === 'boolean';
104 | }
105 |
106 | /**
107 | * Checks if the passed in variable is a function.
108 | * @param {Mixed} obj The variable to check.
109 | * @return {Boolean} True if the variable is a Function.
110 | */
111 | function isFunction(obj) {
112 | return typeof obj === 'function';
113 | }
114 |
115 | /**
116 | * Checks if the passed variable is a function,
117 | * with support for es6 style imports and exports.
118 | * @param {Mixed} obj The variable to check.
119 | * @return {Boolean} True if the variable is a Function,
120 | * or an object with a function in the property "default".
121 | */
122 | function isES6Function(obj) {
123 | return isFunction(obj) || (isObject(obj) && isFunction(obj.default));
124 | }
125 |
126 | /**
127 | * Returns `obj` if `obj` is a function, or `obj.default`
128 | * if obj is a es6-style default function export.
129 | * @param {Mixed} obj The variable to get the function from.
130 | * @return {Function} The function exported from a es6-style module.
131 | */
132 | function getES6Function(obj) {
133 | return isFunction(obj)
134 | ? obj
135 | : isES6Function(obj)
136 | ? obj.default
137 | : null;
138 | }
139 |
140 | /**
141 | * Checks if the passed in variable is a promise.
142 | * @param {Mixed} obj The variable to check.
143 | * @return {Boolean} True if the variable is a promise.
144 | */
145 | function isPromise(obj) {
146 | return typeof obj === 'object' && typeof obj.then === 'function';
147 | }
148 |
149 | /**
150 | * Updates a config object
151 | * @param {Object} oldConfig Old configuration object
152 | * @param {Object} newConfig New configuration object
153 | * @return {Undefined}
154 | */
155 | function changeConfig(oldConfig, newConfig) {
156 | invariant(isObject(oldConfig), 'Old config is not valid');
157 | invariant(isObject(newConfig), 'Config is not valid');
158 |
159 | if (newConfig.host) {
160 | var host = newConfig.host;
161 | var prefix = '';
162 |
163 | if (host.indexOf('http://') < 0 && host.indexOf('https://') < 0) {
164 | prefix += 'http://';
165 | }
166 |
167 | oldConfig.host = prefix + newConfig.host;
168 | }
169 |
170 | if (newConfig.port) {
171 | oldConfig.port = newConfig.port;
172 | }
173 | }
174 |
175 | /**
176 | * Copyright (c) 2014-2015, Facebook, Inc.
177 | * All rights reserved.
178 | *
179 | * This source code is licensed under the BSD-style license found in the
180 | * LICENSE file in the root directory of this source tree. An additional grant
181 | * of patent rights can be found in the PATENTS file in the same directory.
182 | */
183 |
184 | /**
185 | * Use invariant() to assert state which your program assumes to be true.
186 | *
187 | * Provide sprintf-style format (only %s is supported) and arguments
188 | * to provide information about what broke and what you were
189 | * expecting.
190 | *
191 | * The invariant message will be stripped in production, but the invariant
192 | * will remain to ensure logic does not differ in production.
193 | */
194 |
195 | function invariant(condition, format, a, b, c, d, e, f) {
196 | if (format === undefined) {
197 | throw new Error('invariant requires an error message argument');
198 | }
199 |
200 | if (!condition) {
201 | var error;
202 | if (format === undefined) {
203 | error = new Error(
204 | 'Minified exception occurred; use the non-minified dev environment ' +
205 | 'for the full error message and additional helpful warnings.'
206 | );
207 | } else {
208 | var args = [a, b, c, d, e, f];
209 | var argIndex = 0;
210 | error = new Error(
211 | 'Invariant Violation: ' +
212 | format.replace(/%s/g, function() { return args[argIndex++]; })
213 | );
214 | }
215 |
216 | error.framesToPop = 1; // we don't care about invariant's own frame
217 | throw error;
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Isomorphine
2 |
3 | Isomorphine is a webpack loader that lets you access server-side modules in the browser. It works by injecting an rpc routing layer behind the scenes. This lets you require and run server-side code from the browser, without doing AJAX requests or express routes.
4 |
5 | When requiring a server-side entity from the browser, the module is provided as a mirror of the server-side entity. This mirror will automatically transport any method call to the server, and resolve the results in the browser seamlessly. When requiring a module from the server's execution context, the module is resolved as-is, without any mirroring or routing whatsover.
6 |
7 | You can securely share and use server-side code in the browser (for example your database models) and eliminate data fetching boilerplate. The server-side modules will not be required directly in the browser, so you can require modules containing browser-incompatible libraries.
8 |
9 | It does _not_ expose server-side code. It also provides a [security mechanism](#rpc-context) for remote procedure calls (RPCs), and supports promises & async/await.
10 |
11 | You don't need to do HTTP requests and endpoints anymore. You can skip your application's routing layer, and focus on your application's purpose.
12 |
13 |
14 | ### Summary
15 |
16 | * [Usage](#installation)
17 | * [How It Works](#how-it-works)
18 | * [Examples](#examples)
19 | * [Promise & Async/Await support](#promise--es7-support)
20 | * [Security & RPC context](#rpc-context)
21 | * [Caveats](#caveats)
22 | * [Comparison](#comparison)
23 | * [Philosophy](#philosophy)
24 |
25 |
26 | ### Requirements
27 | - Node
28 | - Webpack
29 |
30 | ### Installation
31 |
32 | ```bash
33 | npm install isomorphine
34 | ```
35 |
36 | Then you must [add isomorphine as a webpack loader](#webpack-configuration).
37 |
38 | ### Usage
39 |
40 | Isomorphine has only one method: `isomorphine.proxy()`
41 |
42 | `isomorphine.proxy()` - Creates an object exposing the modules in the directory where it was called. This is similar to [require-all](https://github.com/felixge/node-require-all), but also enables each module to be used in the browser.
43 |
44 | ```js
45 | var isomorphine = require('isomorphine');
46 |
47 | // This will provide the entities in this folder,
48 | // similar to 'require-all' but in a browser-compatible way.
49 | module.exports = isomorphine.proxy();
50 | ```
51 |
52 | Each file in the current directory represents a property in the resulting object. Each file must export a function either in `module.exports` or via `export default`. Directories are scanned recursively (See [require-all](https://github.com/felixge/node-require-all)). When using these modules from the browser through isomorphine, function calls will be proxied to the server.
53 |
54 | This will let you use any server-side entity remotely, through the object created with `isomorphine.proxy()`. Just require this `morphine` object from the browser or the server, and take anything you need out of it. For example:
55 |
56 | ```js
57 | /**
58 | * Suppose the file we defined above is in ./models/index.js,
59 | * and the User model is located in ./models/User.
60 | */
61 | var User = require('./models').User;
62 |
63 | $('#button').on('click', function() {
64 |
65 | // When called from the browser, isomorphine will transport this function call
66 | // to the server, process the results, and resolve this callback function.
67 | // When called from the server, the function is called directly.
68 | User.create({ name: 'someone' }, function(err, user) {
69 | console.log('Im the browser, and I created a user in the db!');
70 | });
71 | });
72 | ```
73 |
74 | You also need to mount isomorphine's RPC interface on your current express-based app, by doing:
75 |
76 | ```js
77 | var express = require('express');
78 | var morphine = require('./models'); // Suppose this is the file we created above
79 |
80 | var app = express();
81 |
82 | // Mounts the rpc layer middleware. This will enable remote function calls
83 | app.use(morphine.router);
84 |
85 | app.listen(3000, function() {
86 | console.log('App listening at port 3000');
87 | });
88 | ```
89 |
90 | Alternatively, you can start isomorphine's router as a stand-alone http server by doing:
91 |
92 | ```js
93 | var morphine = require('./models'); // Suppose this is the file we created above
94 |
95 | // This will enable remote function calls, and must be run in the server
96 | morphine.router.listen(3000, function() {
97 | console.log('RPC interface listening at port 3000');
98 | });
99 | ```
100 |
101 | By default, isomorphine will make the remote procedure calls to the same host and port of the client's current `window.location`. To manually specify the host and port of your API server, you can do:
102 |
103 | ```js
104 | var morphine = isomorphine.proxy();
105 |
106 | morphine.config({
107 | host: 'api.mysite.com', // default is the current browser location hostname
108 | port: '3000' // default is the current browser location port
109 | });
110 | ```
111 |
112 | Remember to read the [caveats](#caveats) for common gotchas, and the section below for a more in-depth explanation.
113 |
114 |
115 | ### How It Works
116 |
117 | * Check the [barebone example](https://github.com/d-oliveros/isomorphine/tree/master/examples/barebone), and the [isomorphic todoMVC](https://github.com/d-oliveros/isomorphic-todomvc) for [full working examples](#examples).
118 |
119 | Isomorphine detects server-side entities by scanning the file structure recursively, and building objects whose methods represent files in the server. Each file has to export a function (via `module.exports` or `export default`). Read the [caveats](#caveats) for common gotchas.
120 |
121 | The internal behavior of `isomorphine.proxy()` differs depending on whether its being ran in the browser or the server:
122 |
123 | * When called from the server: `isomorphine.proxy()` requires all files in the current directory (similar to [require-all](https://github.com/felixge/node-require-all)) and also creates an express-based router that will handle remote procedure calls (RPCs) to the methods in these entities.
124 |
125 | * When called from the browser: `isomorphine.proxy()` creates a mirror to the server-side entities. The mirror is preprocessed and injected by webpack, so you must [add isomorphine as a webpack loader](#webpack-configuration). No router is created in the browser, and no server-side modules are actually `require()`'d in the browser.
126 |
127 | In this example, we will be using a fictitious server-side model called `User`, written in vanilla ES5. We will be splitting each model method in its own file. Please note that only files that export a function can be used in the browser. Promises and ES7 async/await are [also supported](#promise--es7-support).
128 |
129 | ```js
130 | // in /models/User/create.js
131 |
132 | module.exports = function createUser(username, callback) {
133 | var user = {
134 | _id: 123,
135 | type: 'user',
136 | name: username,
137 | created: new Date()
138 | };
139 |
140 | // It doesn't matter how you handle your data layer.
141 | // Isomorphine is framework-agnostic, so you can use anything you want, like
142 | // mongoose, sequelize, direct db drivers, or whatever really.
143 | db.create(user, callback);
144 | }
145 | ```
146 |
147 | ```js
148 | // in /models/User/delete.js
149 |
150 | module.exports = function deleteUser(userId, callback) {
151 | db.remove({ _id: userId }, callback);
152 | }
153 | ```
154 |
155 | To make the these functions available in the browser, you have to create an isomorphic proxy to this folder by using `isomorphine.proxy()`.
156 |
157 | `isomorphine.proxy()` will make these modules available in the browser, without breaking your bundle due to incompatible server-only libraries, bloating your bundle's size, or exposing your server's code. In this example, we will create an isomorphic proxy with `isomorphine.proxy()`, and export it in the index file of the `/models` directory, thus exposing all the models in the browser.
158 |
159 | ```js
160 | // in /models/index.js
161 | var isomorphine = require('isomorphine');
162 |
163 | // This will be our main isomorphic gateway to this folder
164 | module.exports = isomorphine.proxy();
165 | ```
166 |
167 | We are calling `isomorphine.proxy()` in the `index.js` file of the `./models` folder, thus providing all the models in the `./models` folder to the browser, as mirror entity maps.
168 |
169 | Based on this example structure, the object created by `isomorphine.proxy()` is:
170 |
171 | ```js
172 | // The browser gets this ->
173 | {
174 | User: {
175 | create: [func ProxiedMethod],
176 | delete: [func ProxiedMethod]
177 | },
178 | config: [func] // This method sets the host and port of your API server
179 | }
180 |
181 | // Whereas the server gets this ->
182 | {
183 | User: {
184 | create: [func createUser],
185 | delete: [func deleteUser]
186 | },
187 | config: [func emptyFunction] // This method does nothing in the server
188 | router: [func] // this is the RPC API router that must be mounted in your app
189 | }
190 |
191 | // /models/User/create.js and /models/User/delete.js are not actually being
192 | // required in the browser, so don't worry about browser-incompatible modules
193 | ```
194 |
195 | You can use this fictitious `User` model in the browser by doing this, for example:
196 |
197 | ```js
198 | // in /client.js, running in the browser
199 |
200 | // We can interact with this server-side entity (User)
201 | // without manually having to do any HTTP requests.
202 | //
203 | // If you were to require this model directly from the browser, you'd be requiring
204 | // your whole data layer, probably including your database initialization files
205 | // and other modules that are *not* browser-compatible.
206 | //
207 | // By requiring 'User' through '../models/index.js', which is the file where
208 | // we put 'isomorphine.proxy()', we are actually requiring an object
209 | // which mirrors the methods in the real server-side model.
210 | //
211 | // No server-side modules are actually required. Webpack generates an entity
212 | // map before running this code, so it already knows the API surface area of
213 | // your server-side model.
214 | //
215 | // Remember not to require 'User' directly. You need to require User through
216 | // the gateway we created above. Otherwise, you'd be requiring all the model
217 | // dependencies in the browser.
218 | //
219 | var User = require('../models').User;
220 |
221 | // eg. using jquery
222 | $('#button').on('click', function() {
223 |
224 | // When called from the browser, the browser serialized each parameter sent
225 | // to the function call, and sends the serialized payload via a HTTP request
226 | // to Isomorphine's endpoint in the server.
227 | //
228 | // Isomorphine serializes the result of the function call in the server,
229 | // returns the resulting values, deserializes the values, and calls
230 | // this callback function with the resulting values.
231 | User.create('someUser99', function(err, user) {
232 |
233 | window.alert('User created');
234 |
235 | // uhh, lets delete the user better!
236 | User.delete(user._id, function(err) {
237 | window.alert('User deleted');
238 | });
239 | });
240 | });
241 | ```
242 |
243 | To make this work, you must mount the express-based router created by `isomorphine.proxy()` in your app:
244 |
245 | ```js
246 | // in /server.js
247 | var express = require('express');
248 |
249 | // suppose the file we defined above is in ./models/index.js
250 | var morphine = require('./models');
251 |
252 | var app = express();
253 |
254 | // Mounts the rpc layer middleware. This will enable remote function calls
255 | app.use(morphine.router);
256 |
257 | // you can mount other middleware and routes in the same app
258 | // app.get('/login', mySecureLoginCtrl);
259 |
260 | app.listen(3000, function() {
261 | console.log('Server listening at port 3000');
262 | });
263 |
264 | // Alternatively, if you don't wish to mount isomorphine's middleware in your app,
265 | // you can just start listening for RPCs by doing
266 | var morphine = require('./models');
267 | morphine.router.listen(3000, function() {
268 | console.log('Server listening at port 3000');
269 | });
270 | ```
271 |
272 | Multiple arguments in exported functions are supported. You can define and use functions as you would normally do:
273 |
274 | ```js
275 | // in /client.js, running in the browser
276 | var User = require('./models').User;
277 |
278 | User.edit(user._id, { name: 'newName' }, 'moreargs', (err, editedUser, stats) => {
279 | console.log('User edited. Server said:');
280 | console.log(editedUser);
281 | console.log(stats);
282 | });
283 | ```
284 |
285 | **Your server's files will _not_ be exposed in the browser, nor they will get added to the bundled file.**
286 |
287 | It also supports promises and ES7 async/await
288 |
289 | ```js
290 | // in /client.js, running in the browser
291 | import { User } from '../models';
292 |
293 | $('#button').on('click', async () => {
294 |
295 | // using promise-based server-side entities, and async/await in the browser
296 | const user = await User.create('someUser99');
297 | window.alert('User created');
298 |
299 | // lets delete this user
300 | await User.delete(user._id);
301 | window.alert('User deleted');
302 | });
303 | ```
304 |
305 | Other than reducing boilerplate code, it really shines in isomorphic applications, where you need the same piece of code running in the browser and the server, for example when doing server-side rendering of a react application.
306 |
307 | ```js
308 | import { User } from '../models';
309 |
310 | // In the browser, calling 'User.get()' will actually make a HTTP request
311 | // to the server, which will make the actual function call,
312 | // serialize the results back to the browser, and resolve the promise with the value(s).
313 | //
314 | // In the server, 'User.get()' will be called directly
315 | // without doing any HTTP requests or any routing whatsoever.
316 | //
317 | // This same piece of code can be run seamlessly in the browser and the server
318 | export default class MyComponent extends React.Component {
319 | async componentDidMount() {
320 | const user = await User.get(123);
321 | this.setState({ user });
322 | }
323 | }
324 | ```
325 |
326 | Please read the [caveats](#caveats) for common gotchas, and the section below for working examples.
327 |
328 |
329 | ### Examples
330 |
331 | * [Barebone](https://github.com/d-oliveros/isomorphine/tree/master/examples/barebone) - Barebone example using express, jquery, webpack.
332 | * [Isomorphic React](https://github.com/d-oliveros/isomorphine/tree/master/examples/isomorphic-react) - Server-side rendered React example using React, [Baobab](https://github.com/Yomguithereal/baobab), [Babel](https://github.com/babel/babel).
333 | * [isomorphic TodoMVC](https://github.com/d-oliveros/isomorphic-todomvc) for a full isomorphic TodoMVC react example.
334 |
335 | Also go to [Wiselike](https://wiselike.com) to see it running in a production environment, and [ask me anything here!](https://wiselike.com/david)
336 |
337 |
338 | ### Webpack Configuration
339 |
340 | In order for Isomorphine to work, you need to specify Isomorphine as a webpack loader in your `webpack.config.js` file. The main `isomorphine` package contains the webpack loader so you don't need to install anything else.
341 |
342 | ```js
343 | module.exports = {
344 | entry: {...},
345 |
346 | module: {
347 | preLoaders: [{ loaders: ['isomorphine'] }]
348 | },
349 | ...
350 | };
351 | ```
352 |
353 | For instructions and a usage guide, please read [Usage](#usage)
354 |
355 |
356 | ### Promise / ES7 Support
357 |
358 | Isomorphine supports promises, async/await, and callback-based functions.
359 |
360 | ```js
361 | // Promise support
362 | module.exports = function getUser() {
363 | return db.findAsync({ _id: 123 }); // supposing this returns a promise
364 | }
365 | ```
366 |
367 | ```js
368 | // Another promise support example
369 | module.exports = function readSomeFile() {
370 | return new Promise((resolve, reject) => {
371 | fs.readFile('somefile.txt', function(err, file) {
372 | if (err) return reject(err);
373 | resolve(file);
374 | });
375 | });
376 | }
377 | ```
378 |
379 | ```js
380 | // ES7 async/await support
381 | export default async function getUser() {
382 | const user = await MyUserModel.find({ _id: 123 });
383 |
384 | // await doMoreStuff()...
385 |
386 | return user;
387 | }
388 | ```
389 |
390 | ```js
391 | // callback-based support
392 | module.exports = function getUser(uid, callback) {
393 | MyUserModel.find({ _id: uid }, (err, user) => {
394 | if (err) return callback(err);
395 |
396 | console.log('Got a user!');
397 |
398 | callback(null, user);
399 | });
400 | }
401 | ```
402 |
403 |
404 | ### RPC Context
405 |
406 | Allowing any API endpoint to be called from the browser, needs a proper validation mechanism to avoid getting exploited easily.
407 |
408 | When a call to a server-side function is done from the browser, a special context is passed to the function call. A special `xhr` flag and the request object `req` are passed as the function's context, in `this.xhr` and `this.req`:
409 |
410 | ```js
411 | /**
412 | * When an endpoint is called from the browser, 'this.xhr' will be true,
413 | * and you'll be able to access the request object in 'this.req'.
414 | */
415 | module.exports = function createUser(username, callback) {
416 | if (this.xhr) {
417 | console.log('This function is being called remotely!');
418 | console.log('Request is', this.req);
419 | }
420 |
421 | myUserModel.create(username, callback);
422 | }
423 | ```
424 |
425 | You can use this context to validate incoming requests. Please note, Isomorphine is unobtrusive and comes with no security middleware by default (other than this mechanism).
426 |
427 | You *must* implement your own security mechanism yourself in an earlier middleware stage (using cookies or redis sessions or JWT or w/e):
428 |
429 | ```js
430 | module.exports = function deleteUser(userId, callback) {
431 |
432 | // Suppose I have a previous middleware step that adds `isAdmin`
433 | // to the request object, based on the user's session or else
434 | if (this.xhr && !this.req.isAdmin) {
435 | return callback(401);
436 | }
437 |
438 | myUserModel.delete(userId, callback)
439 | }
440 | ```
441 |
442 | If the function is not being called remotely, `this.req` will be null, so make sure to validate `this.xhr` before trying to do something with the request object `this.req`.
443 |
444 |
445 | ### Caveats
446 |
447 | * Your files must export a function in `module.exports` (or `export default` if using ES6 syntax) if you want to be able to call them from the browser. There is currently no support for exporting objects yet. If anyone figures out a way to determine the exports of a module without requiring its dependencies I'll be happy to merge the PR :D
448 |
449 | * Your modules have to be required through the isomorphine proxy. You can not require a server-side entity directly. If you do, you will be importing all the server-side code to the browser's bundle, and possibly breaking your app due to browser-incompatible modules, like `fs`, `express`, `mongo`, database drivers, etc.
450 |
451 | * When a function is called directly from the server (eg when called by a cron job), there's no `this.req` object being passed to the function calls, so you must validate sensitive paths in an earlier stage.
452 |
453 | * Also, please note that the file where you create the isomorphic proxy, has to be browser-compatible. Isomorphine works by proxying the methods contained in the specified directory, but the file itself will be required as-is in the browser.
454 |
455 |
456 | ### Comparison
457 |
458 | The two examples below achieve the same result. They both create a web server, register a route that returns a user by user ID, starts listening at port 3000, and call this endpoint from the browser.
459 |
460 | One example uses isomorphine, while the other one uses the common approach of building an API route, calling a model from a controller, and building a client-side wrapper for data-fetching logic.
461 |
462 | ##### With isomorphine:
463 |
464 | ```js
465 | // in ./api/User/get.js
466 |
467 | // Dummy User.get() method
468 | module.exports = function(id, callback) {
469 | var user = {
470 | id: id,
471 | name: 'Some User'
472 | };
473 |
474 | callback(null, user);
475 | };
476 | ```
477 |
478 | ```js
479 | // in ./api/index.js
480 | var isomoprhine = require('isomorphine');
481 |
482 | // make the API entities callable from the browser
483 | var morphine = isomorphine.proxy();
484 |
485 | // start listening for RPC calls
486 | morphine.router.listen(3000, function() {
487 | console.log('Interface listening at port 3000');
488 | });
489 |
490 | module.exports = morphine;
491 | ```
492 |
493 | ```js
494 | // in ./client.js
495 | var User = require('./api').User;
496 | var userId = 123;
497 |
498 | // just use the model and be happy
499 | User.get(userId, function(err, user) {
500 | console.log('User is', user);
501 | });
502 | ```
503 |
504 |
505 | ##### Without isomorphine:
506 |
507 | ```js
508 | // in ./api/User/get.js
509 |
510 | // Dummy User.get() method
511 | module.exports = function(id, callback) {
512 | var user = {
513 | id: id,
514 | name: 'Some User'
515 | };
516 |
517 | callback(null, user);
518 | };
519 | ```
520 |
521 | ```js
522 | // in ./api/index.js
523 | var User = require('./User');
524 |
525 | var express = require('express');
526 | var bodyParser = require('body-parser');
527 | var http = require('http');
528 |
529 | var app = express();
530 |
531 | app.use(bodyParser.urlencoded());
532 | app.use(bodyParser.json());
533 |
534 | app.get('/api/user/:id', function(req, res, next) {
535 | var userId = req.params.id || 123;
536 |
537 | User.get(userId, function(err, user) {
538 | if (err) {
539 | return next(err);
540 | }
541 | res.set('Content-Type', 'application/json');
542 | res.send(user);
543 | });
544 | });
545 |
546 | var server = http.createServer(app);
547 | server.listen(3000, function() {
548 | console.log('Server listening at port 3000');
549 | });
550 | ```
551 |
552 | ```js
553 | // in ./client.js
554 | var request = require('request');
555 | var userId = 1;
556 |
557 | // unnecesary boilerplate code
558 | function getUser(id, callback) {
559 | request.get(`/api/user/${id}`, function(err, res) {
560 | if (err) {
561 | return callback(err);
562 | }
563 | var user = res.data;
564 | callback(null, user);
565 | });
566 | }
567 |
568 | // gets the user
569 | getUser(userId, function(err, user) {
570 | console.log('User is', user);
571 | });
572 | ```
573 |
574 |
575 | ##### With Isomorphine (condensed):
576 |
577 | ```js
578 | var isomoprhine = require('isomorphine');
579 | var morphine = isomorphine.proxy();
580 | morphine.router.listen(3000, function() {
581 | console.log('Interface listening at port 3000');
582 | });
583 | module.exports = morphine;
584 | var User = require('./api').User;
585 | var userId = 123;
586 | User.get(userId, function(err, user) {
587 | console.log('User is', user);
588 | });
589 | ```
590 |
591 |
592 | ##### Without Isomorphine (condensed):
593 |
594 | ```js
595 | var User = require('./User');
596 | var express = require('express');
597 | var bodyParser = require('body-parser');
598 | var http = require('http');
599 | var app = express();
600 | app.use(bodyParser.urlencoded());
601 | app.use(bodyParser.json());
602 | app.get('/api/user/:id', function(req, res, next) {
603 | var userId = req.params.id || 123;
604 | User.get(userId, function(err, user) {
605 | if (err) {
606 | return next(err);
607 | }
608 | res.set('Content-Type', 'application/json');
609 | res.send(user);
610 | });
611 | });
612 | var server = http.createServer(app);
613 | server.listen(3000, function() {
614 | console.log('Server listening at port 3000');
615 | });
616 | var request = require('request');
617 | var userId = 1;
618 | function getUser(id, callback) {
619 | request.get(`/api/user/${id}`, function(err, res) {
620 | if (err) {
621 | return callback(err);
622 | }
623 | var user = res.data;
624 | callback(null, user);
625 | });
626 | }
627 | getUser(userId, function(err, user) {
628 | console.log('User is', user);
629 | });
630 | ```
631 |
632 | This example took 11 lines of code using Isomorphine. Doing this the traditional way took 35 lines. _And its only one route_
633 |
634 | If we were to add more CRUD routes to our model, each route would require:
635 |
636 | 1. A controller
637 | 2. Some request validation
638 | 3. A method in your model (maybe)
639 | 4. Some wrapper in your client-side application
640 |
641 | Plus having to mantain these new components. Multiply this for each route you are currently mantaining, and you'll realize there has to be a better way to streamline your application's development.
642 |
643 | With isomorphine, you can just call the server-side model directly. The model is already being supplied to the browser, so you would only require:
644 |
645 | 1. A new method in your model
646 |
647 | No need to re-write the data-fetching layer in the client application, or get parameters out of request object. heck, you don't even need to define and mantain routes. If you need to access the request object for validation purposes or else, you can access it through `this.req` as specified [here](#rpc-context).
648 |
649 | _Disclaimer: I'm not saying Isomorphine is the best fit for every case. You should have your routes and middleware in place to serve the client and views, and to handle special routes like auth actions, and should be providing everything you need to correctly authenticate remote calls to your methods. Starting isomorphine directly from `morphine.router.listen()` is not recommended, as Isomorphine is only intended to handle RPCs to server methods. It is not meant to server as a full-blown HTTP server or application stack._
650 |
651 |
652 | ### Philosophy
653 |
654 | Isomorphine proposes an endpoint-less API approach in an attempt to further abstract the barriers between the server and the browser. It is meant to increase code reusability between the server and the browser, specially in an isomorphic full-stack javascript environment.
655 |
656 | The idea is to encapsulate the routing layer within javascript's native syntax for importing and exporting modules, while providing a middleware interface to let you mount its RPC handler the way you want. This massively reduces development times, as you don't have to worry about connecting the browser and server together, so you can focus solely in your application's purpose.
657 |
658 | The original idea was to use ES6's `Proxy` in the browser, to proxy any function call from any property of any object existing in any file in the server. Unfortunately, I quickly found out that there was no out-of-the-box support for `Proxy` in most of the major browsers. This led to the idea of using Webpack to pre-generate a map of server-side entities based on filenames. While this work, full support for any type of export will probably come after a wider adoption of ES6's `Proxy` in the browser, or a more advanced webpack loader.
659 |
660 |
661 | ### Tests
662 |
663 | ```bash
664 | mocha test
665 | ```
666 |
667 | Cheers.
668 |
--------------------------------------------------------------------------------