marooka
v0.2.2
Published
Marooka: Serving Content on Koa server with Marko and Lasso
Downloads
28
Readme
Markoa
Note: This project is referenced as marooka
since markoa
was already taken on npm
.
Markoa consists of the following main parts:
- app
- app configurator
- app container
- app mounter
- components
- config
- server
- templates
- utils
Use the main markoa
object as follows:
let markoa = require('markoa');
let app = markoa.app;
See lib/index
for the full external API.
Server
It is super simple to mount a basic app on Markoa if it follows conventions.
let marooka = require('marooka')
let apps = ['index', 'projects', 'login']
let rootPath = __dirname
module.exports = new marooka.AppMounter(rootPath).mountApps(apps).start();
By default the koa server will use port 4000
if no extra settings are provided.
App file structure
For any custom app structure, you may supply your own findTemplate
and pageData
functions in the app config
Object when mounting your apps.
The project file structure should look as follows.
/apps
/_global
/components - custom taglibs and tags
/tags
/feed
/project-feed
marko-tag.json
renderer.js
template.jade
template.marko
marko-taglib.json
/widgets
marko-taglib.json
marko-taglib.json
/data - data available to all apps as $out.global
index.js
/available
index.js
categories.js
...
/feeds
index.js
/layouts - generic layouts
layout.jade
item-layout.jade
list-layout.jade
/index - app
meta.js - app meta data
/components - app specific components
marko-taglib.json
/layouts - special page layouts
mobile.jade
base.jade
/page - page for app
app.jade
app.marko
/dependencies
broser.json
app.browser.json - lasso config file
widgets.json
/data - data for index app, available as $data
global.js - reuse global data from local app
index.js - local data for index app only
marko-taglib.json
/repositories - app
meta.js
...
/teams - app
...
marko-taglib.json
Generating apps
The generator slush-markoa can be used to generate projects and other artifacts.
Generate a new app:
slush markoa:app
This generator will create an app under apps/[app-name]
similar to the default index
app generated by the default marko generator. Use this generator each time you want to add an app!
/[app]
/components
/tags
/project-feed
template.jade
...
/layouts
base.jade
/data
global.js
index.js
/page
meta.js
app.jade
app.marko
/dependencies
app.browser.json
marko-taglib.json
Sub pages
TODO: WIP
Sometimes an app contains sub pages. To support this, markoa should be able to understand how to mount sub-routes for a given app. Only two levels are allowed and possible at this point.
/users
/page
app.jade
app.marko
/pages
details.jade
feed.jade
/dependencies
app.browser.json
If a /pages
folder exists, Markoa should add a route for each page there, such as users/details
and users/feed
. All pages for the app share the root page data by default, however the sub-pages can extend or override this data by a page/pages
object where each entry is the data specific to a subpage. In short, the page found at page/pages/details
will get the data from page
extended (and potentially overwritten) by page/pages/details
.
module.exports = {
page: {
name: 'users',
title: 'Users',
pages: {
details: {
title: 'Detailed Users',
caption: 'Your favorites'
},
feed: {
// ...
}
}
},
}
When the page users/details
is rendered, it will get the data:
{
name: 'users',
title: 'Detailed Users', // overrides
caption: 'Your favorites' // extends
}
To achieve this we need to look in app-config.js
config.app = new App(name, config);
return {name: name, config: config};
in Router
return function(pageName, config) {
log('create route', pageName, config);
let app = config.app;
let routeName = pageName;
...
route(routeName, config);
and in Route
where the GET route is added to the app
let page = config.app;
App
should return a nested collection of page configs instead of returning a single page config. This nested config should then be iterated and a route (with data etc.) generated for each.
This infrastructure change has already been started in App in:
- page/pages.js
- resolver/index.js
- resolver/pages/
The app
returned by the App
constructor, now includes an apps
entry. If we call app.apps.sub()
we get the sub apps object for the app.
For the router, we must return a nested set of routers, one for each nested sub app and not a single router!
var router = function(pageName, config) {
log('create routes for app:', pageName);
let app = config.app;
console.log('routes', app.apps.sub());
...
}
return router;
App Meta data and inheritance
An app folder can contain a meta.js
file to define meta data for the app.
module.exports = {
type: 'item', // or: home, list, ...
form: true, // if it contains a form to edit the item
inherit: 'item', // app to inherit from for all
page: {
// type: 'item',
app: 'item' // app to use for page if no page found here
},
data: {
// type: 'item',
// app: 'item' app to use as data source if no data here
}
}
Inheritance and reuse
The meta data can be used to indicate which apps to fall back to (inherit from) for the template and data used so as to reuse from other apps and thus minimize duplication.
Stats
The meta data can also be used to gather stats about the app in aggregate, f.ex to list all the apps that display lists, have forms etc.
Using App inheritance
Let's say we have global data:
lists: {
projects: [...],
teams: [...]
}
Ideally we would like to have this global data available for reference but also to reuse this data at the app level as local data. To enable this, each local /data
folder has a global.js
which exports all the global data which can then be referenced locally as follows.
var _ = require('./global');
module.exports = {
// See global data, lists/projects
// out.global.lists.projects
page: {
name: 'projects',
title: 'Projects',
list: _.lists.projects;
}
}
So here we set up a local generic list
to point to the global data list.projects
. This can then be passed to whatever list generator which knows how to populate and render a given type of list. Magic!
Using this approach, any app which which displays a model or list using the same renderer, can be set up to inherit from a generic app which handles it.
module.exports = {
type: 'list',
page: {
app: 'list'
pages: {
details: {
app: 'list/details'
},
feed: {
app: 'list/feed'
}
}
}
}
Since this pattern is so common you can do the shorthands:
app: 'list'
pages: {
details: {
app: ':details'
},
Or even shorter:
app: 'list'
pages: 'list' // or 'inherit' to use same inheritance as app
App
An App
is simply an Object with a specific structure that defines where or how specific "endpoints" of the app can be retrieved, such as the main page template and the page data (data) of the app. An app can also contribute to the global data via the special $global
entry. There are several ways to create an app:
let app = {
rootPath: __dirname,
page: {
data: {
page: function(name, config) {},
$global: function(name, config) {}
},
template: function(name) {
return 'path/to/template';
}
}
};
let config = {rootPath: __dirname, page: {template: 'path/to/template' }}
let myApp = new markoa.app.create(name, config);
App container
You can mount one or more apps directly on your AppContainer
let myApp = new markoa.App('project', projConfig);
let myAppContainer = new markoa.AppContainer(koaApp); //.start();
myAppContainer.mount.app(myApp);
App configurator
You can use the AppConfigurator
to configure multiple apps to be mounted on an AppContainer
. The AppConfigurator
uses config objects to mount an app using either default stretegies for resolving the main page template and data of each app, or custom strategies you supply.
You can customize the configurator as needed, then call mountApps
with the list of apps you wish to mount on the app container.
let lassoFile = path.join(__dirname, './lasso-config.json');
let serverOpts = {port: 4005, lassoFile: lassoFile};
let koaApp = new Server(serverOpts).init(function(mws) {
// configure koa middleware
mws.minimal();
});
// merge apps (app configurations) from another AppContainer
appContainer.join(otherAppContainer);
// container is optional. If not supplied, a new one will be created
let appConfigurator = new markoa.AppConfigurator({rootPath: __dirname, container: appContainer});
let apps = ['project', 'repository'];
// mounting multiple apps on appContainer instance
appConfigurator.mountApps(apps);
// creates routes for all apps in container and starts server
appContainer.start(koaApp);
App Mounter
For the simplest cases, simply use the AppMounter
like this:
var mounter = new markoa.AppMounter(__dirname);
mounter.mountApps(apps);
mounter.appContainer.start(koaApp);
App data
An /apps
folder being mounted, can contribute to the global data of the app container where it mounts. You should have a file apps/_global/data.js
or more typically apps/_global/data/index.js
which returns an Object or a function of the form function(name, config)
, where name is the name of the current app trying to access global data and config is a config object.
Each app on its own should also have a data, such as for the index
app, either: apps/index/data.js
or apps/index/data/index.js
adhering to the same rules as for global data.
For more advanced scenarios, you can even provide different data for each environment: development
, test
and production
, simply by having top level data object keys for each such environment you wish to support. You can provide a default:
key for default data for environment not defined, if none of these keys are found it will default to retrieve the entire data (data).
Local testing
Run npm link
from markoa root folder to link the package.
Then from an app or appContainer that uses markoa, use npm link markoa
to link the dependency, which creates a symbolic link in your node_modules
pointing to your local markoa package.
Now run npm install
from your app ;)
Widget dependency management for client app
See the new lib/components
folder
Components can generate registries of Components for the app (global, per app). It can also be used to uncover which tags are in fact Widgets and store them in a widgets.json
file for each app. This can file can then be used as input to generate an [app-name].browser.json
for each app with all widget dependencies auto-magically pre-configured!!!
Server Templates
The lib/templates
folder contains a script which can compile Marko templates into Liquid templates for use on the server.