Skip to content
Example of using Async Tree Pattern for building web application
Branch: master
Clone or download
Latest commit 71f2eb6 May 11, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
async
pages udpate cutie + stabilize tests Feb 23, 2019
server fix vuls and update deps May 11, 2019
static fix vuls and update deps May 11, 2019
templates simple cute page Dec 17, 2018
test/server
.babelrc
.eslintrc.json udpate cutie + stabilize tests Feb 23, 2019
.gitignore fix .gitignore Nov 20, 2018
.travis.yml fix vuls and update deps May 11, 2019
Gruntfile.js
LICENSE Initial commit Sep 19, 2018
README.md fix vuls and update deps May 11, 2019
build.js
config.json
package-lock.json
package.json
test.js

README.md

NPM Version Build Status codecov

Page can be described as a base for any applications on top of it with server and client in Node.js. It provides a lot of features and common scenarios for using web. It's completely based on the Async Tree Pattern that allows you to customize Page in any way you want, you can even throw it away and build other base core for your application.

In another words, Page is just an example of how you can build your application using libraries that are based on cutie.

Contents

Basic Concepts

  1. Page Is Not a Regular Framework. Almost every framework is made with assumption that we live in ideal world, but it's very far from the truth. It's not possible to build something big and stable using magic, which every framework is based on. Meanwhile, Page allows you to control the whole behaviour of your application and apply new changes in a explicit way.

  2. It's Not Easy to Start Quickly. First of all you need to get acquainted with Async Tree Pattern and it's implementation. It allows to build everything using pure approach in OOP. Also, you must know how Node.js works and it's important to understand how non-blockinng i/o works there.

  3. But It's Very Easy to Continue. Ones you've learnt how to use Async Tree Pattern, libraries that are based on cutie and libraries for Page, your life will be never like before. You'll able to intoduce new changes into your code extremely fast and painless(unlike in other frameworks).

  4. No Unnecessary Complexity. Only html, css and js (server side and browser).

  5. Small Core. Page is almost based on little pieces from different libraries that can be easily combined with each other for building appication. It makes Page lightweight and easily extensible.

How To Start (page-cli)

First of all you need to download this repository to your local machine. You can do it via github or page-cli. We suggest you to use the last option, because it also makes building and running of your application much easier.

Installation Instructions

  1. Install page-cli: npm install @page-libs/cli -g
  2. Go to the workspace where you want to create your project: cd ../<my-projects>
  3. Create project: page create
  4. Then you'll have to enter some information about your project (Project name, Version, Author, Description, License), you'll get this repository with changed package.json and removed .git directory (so you can bind it to your project on github).
  5. Go to your project directory: cd <projectName>
  6. Install dependencies: npm install

Update Instructions

  1. Go to your project directory: cd <project_name>
  2. Update version of framework: page update, this command just updates versions of components in your package.json
  3. Install new dependencies: npm install

Test Instructions

  1. Go to your project directory: cd <project_name>
  2. Run tests: page test (it runs npm test)

Project Structure

├── ProjectName
│   ├── async
│   ├── pages
│   │   ├── **/*.js
│   ├── server
│   │   ├── async
│   │   ├── endpoints
│   │   ├── events
│   │   ├── api.js
│   │   ├── run.js
│   │   ├── tunedWatchers.js
│   ├── static
│   │   ├── css
│   │   │   ├── **/*.css
│   │   ├── html
│   │   │   ├── **/*.html
│   │   ├── image
│   │   │   ├── **/*.{jpg,png,..}
│   │   ├── js
│   │   │   ├── **/*.js
│   │   ├── txt
│   │   │   ├── **/*.txt
│   │   ├── ...
│   ├── templates
│   │   ├── **/*.html
│   ├── test
│   ├── ├── server
│   ├── ├── ├── files
│   ├── ├── ├── ├── **/*.{html, js}
├── ├── ├── ├── **/*.js
│   ├── .babelrc
│   ├── .eslintrc.json
│   ├── build.js
│   ├── .gitignore
│   ├── config.json
│   ├── Gruntfile.js
│   ├── LICENSE
│   ├── package-lock.json
│   ├── package.json
│   ├── README.md
│   ├── test.js

async directory

This directory contains async objects for the whole application.

pages directory

This directory contains js scripts that generate static html pages(they are based on page-static-generator library).

server directory

This directory contains server part of the application. It contains build script and run script.

server/async directory

This directory contains async objects that we use in server/build.js and server/run.js.

server/endpoints

This directory contains endpoints for REST API.

server/events

This directory contains events for different purposes.

static directory

This directory contains static files, each type of files is stored in the corresponding subdirectory(html, js and so on). You can also add subdirectories for different extensions. Just don't forget to configure it in the running process.

templates directory

This directory contains html tepmlates(components) for generating pages.

test directory

This directory contains tests on async objects in server directory.

config.json

config.json contains all settings of Page. Use following async composition for retrieving values from config in the code:

const { ParsedJSON, Value } = require('@cuties/json')
const { ReadDataByPath } = require('@cuties/fs')

new ParsedJSON(
  new ReadDataByPath('./config.json')
).as('config').after(
  /* 
    now you can use following composition 
      for retrieving concrete value from config
  */
  new Value(as('config'), 'somePropertyName')
).call()

Properties in config.json

page

"page": {
  "version": "1.0.0",
  "logoText": "./static/txt/logo.txt",
  "logoImage": "./static/image/logo.png",
  "logoImageSrc": "/../image/logo.png"
}

This property contains object with information about Page: current version, path to the logo in the text format and image format, also image address of the logo.

index, indexHref

These properties point to the index page. index is for location of the index page in the file system and indexHref is for link address of the index page.

static

This property is for location of the directory of static files.

staticGenerators

This property is for location of the directory with static genrators.

templates

This property is for location of the directory with templates.

staticHtml

This property is for location of the directory of static files with html extension.

staticJs

This property is for location of the directory of static files with js extension (es6).

outStaticJs

This property is for location of the directory of static files with js extension(for using in a browser).

bundle

This property is for location of the bundle file that is generated from outStaticJs directory.

bundleHref

This property is for link of the bundle file.

minBundle

This property is for location of the minified version of the bundle file.

minBundleHref

This property is for link of the minified bundle file.

enviroments(local, prod, dev, stage, prod)

Every environment property includes protocol, port, host, clusterMode. You can also add your own environments and environment properties (for example, if you use https for protocol, you might need cert and key properties).

"local": {
  "protocol": "http",
  "port": 8000,
  "host": "127.0.0.1",
  "clusterMode": true
},
"dev": {
  "protocol": "http",
  "port": 8000,
  "host": "127.0.0.1",
  "clusterMode": true
},
"test": {
  "protocol": "http",
  "port": 8000,
  "host": "127.0.0.1",
  "clusterMode": true
},
"stage": {
  "protocol": "http",
  "port": 8000,
  "host": "127.0.0.1",
  "clusterMode": true
},
"prod": {
  "protocol": "http",
  "port": 8000,
  "host": "127.0.0.1",
  "clusterMode": true
}

.eslintrc.json

It's a default config for eslint. You can customize it via command: ./node_modules/.bin/eslint --init

Gruntfile.js

Default cofiguration for grunt build system. You can find more information in this section.

test.js

This script executes all tests in the test directory using this library.

Building Process

The declaration of this process is in server/build.js script. Here we execute static analysis (for pages, server, static/js/es6 and test packages), test coverage of test script and grunt build (you can use other build system). After grunt tasks are executed we generate static pages. And that's it, you can also add some other steps in your building process.

// build.js

const { as } = require('@cuties/cutie')
const { Value } = require('@cuties/json')
const { ExecutedScripts } = require('@cuties/scripts')
const { SpawnedCommand } = require('@cuties/spawn')
const { ExecutedLint, ExecutedTestCoverage, ExecutedTestCoverageCheck } = require('@cuties/wall')
const Config = require('./async/Config')
const PrintedStage = require('./async/PrintedStage')
const env = process.env.NODE_ENV || 'local'

new Config('./config.json').as('config').after(
  new Config('./package.json').as('packageJSON').after(
    new PrintedStage(as('config'), as('packageJSON'), `BUILD (${env})`).after(
      new ExecutedLint(process, './pages', './server', './static/js/es6', './test').after(
        new ExecutedTestCoverageCheck(
          new ExecutedTestCoverage(process, './test.js'),
          { 'lines': 100, 'functions': 100, 'branches': 100 }
        ).after(
          new SpawnedCommand('grunt').after(
            new ExecutedScripts(
              'node', 'js', new Value(as('config'), 'staticGenerators')
            )
          )
        )
      )
    )
  )
).call()

Static analysis and test coverage (wall)

You can find information about configuring async composition of static analysis and test coverage here.

Also you can customize eslint config via command ./node_modules/.bin/eslint --init. Default configuration you can find here.

Running Process

The declaration of this process is in server/run.js script.

Backend (server)

For building backend server with REST API we use here cutie-rest:

// server/async/LaunchedBackend.js

const { Backend } = require('@cuties/rest')
const { Value } = require('@cuties/json')
const Api = require('./Api')
const env = process.env.NODE_ENV || 'local'

module.exports = class {
  constructor (config) {
    return new Backend(
      new Value(config, `${env}.protocol`),
      new Value(config, `${env}.port`),
      new Value(config, `${env}.host`),
      new Api(config)
    )
  }
}

Where Api object is for our REST API, which is defined in the ./server/async/Api.js script:

// server/async/Api.js

const { RestApi, ServingFilesEndpoint, CachedServingFilesEndpoint } = require('@cuties/rest')
const { Value } = require('@cuties/json')
const { Created } = require('@cuties/created')
const CustomIndexEndpoint = require('./../endpoints/CustomIndexEndpoint')
const CustomNotFoundEndpoint = require('./../endpoints/CustomNotFoundEndpoint')
const CustomInternalServerErrorEndpoint = require('./../endpoints/CustomInternalServerErrorEndpoint')
const UrlToFSPathMapper = require('./UrlToFSPathMapper')
const env = process.env.NODE_ENV || 'local'
const headers = env === 'prod' ? { 'Cache-Control': 'cache, public, max-age=86400' } : {}
const servingFilesEndpoint = env === 'prod' ? CachedServingFilesEndpoint : ServingFilesEndpoint

class CreatedCustomNotFoundEndpoint {
  constructor (config) {
    return new Created(
      CustomNotFoundEndpoint,
      new RegExp(/^\/not-found/),
      new Value(config, 'notFoundPage')
    )
  }
}

module.exports = class {
  constructor (config) {
    return new RestApi(
      new Created(
        CustomIndexEndpoint,
        new Value(config, 'index'),
        new CreatedCustomNotFoundEndpoint(config)
      ),
      new Created(
        servingFilesEndpoint,
        new RegExp(/^\/(css|html|image|js|txt)/),
        new UrlToFSPathMapper(
          new Value(config, 'static')
        ),
        headers,
        new CreatedCustomNotFoundEndpoint(config)
      ),
      new CreatedCustomNotFoundEndpoint(config),
      new CustomInternalServerErrorEndpoint(new RegExp(/^\/internal-server-error/))
    )
  }
}

More information about declaration REST API you can find in the docs cutie-rest.

The Whole Declaration

I believe that the declarative code below is self-explainable, but you can anyway submit an issue, if something is not clear. However, it requires some knowledge in such modules like: cutie, cutie-if-else, cutie-cluster, cutie-json, cutie-rest, cutie-fs.

// server/run.js

const cluster = require('cluster')
const { as } = require('@cuties/cutie')
const { If, Else } = require('@cuties/if-else')
const { IsMaster, ClusterWithForkedWorkers, ClusterWithExitEvent } = require('@cuties/cluster')
const { Value } = require('@cuties/json')
const Config = require('./../async/Config')
const PrintedStage = require('./../async/PrintedStage')
const ReloadedBackendOnFailedWorkerEvent = require('./events/ReloadedBackendOnFailedWorkerEvent')
const LaunchedBackend = require('./async/LaunchedBackend')
const TunedWatchers = require('./async/TunedWatchers')

const numCPUs = require('os').cpus().length
const env = process.env.NODE_ENV || 'local'
const devEnv = env === 'local' || env === 'dev'

new Config('./config.json').as('config').after(
  new Config('./package.json').as('packageJSON').after(
    new If(
      new IsMaster(cluster),
      new PrintedStage(as('config'), as('packageJSON'), `RUN (${env})`).after(
        new If(
          devEnv,
          new TunedWatchers(as('config'))
        ).after(
          new If(
            new Value(as('config'), `${env}.clusterMode`),
            new ClusterWithForkedWorkers(
              new ClusterWithExitEvent(
                cluster,
                new ReloadedBackendOnFailedWorkerEvent(cluster)
              ), numCPUs
            ),
            new Else(
              new LaunchedBackend(as('config'))
            )
          )
        )
      ),
      new Else(
        new LaunchedBackend(as('config'))
      )
    )
  )
).call()

In few words, here running process runs a server with REST API (in the cluster mode by default) and attaches fs watchers on pages, static, templates directories(in local and dev environments). FS watchers are declared by TunedWatchers object which is defined in this script. Also in the cluster mode failed processes restart automatically.

As you can see here, we get some parameters like post and host from config.json. Also, you can notice that it's possible to run server in cluster mode.

It's just an example of how it could be built and worked. But, of course, you can configure it differently due to your requirements (it's quite configurable code).

Build Project

For building use command: page build [evironment] or page b [evironment]. environment is one of the following values: local, prod, dev, stage, prod (local is the default environment).

Run Project

For running use command: page run [evironment] or page r [evironment]. environment is one of the following values: local, prod, dev, stage, prod (local is the default environment).

Test Project

You can do it via page test or just npm test.

List of Libraries For Page

All these libraries are available on npm under @page-libs scope.

page-cutie

This library is analogue of cutie for using Async Tree Pattern in browser.

You can use async object from page-cutie with async objects from cutie on the server(Node.js programm) with no problems. But if you want to use mixed async objects in browser, you must transform cutie async objects to page-cutie async objects. Use following function:

const PageAsyncObject = require('@page-libs/cutie').AsyncObject
const { AsyncObject1, AsyncObject2, AsyncObject3 } = require('some-lib')

// ...asyncObjects extend AsyncObject from cutie
function transformAsyncObjects(...asyncObjects) {
  for (let i = 0; i < asyncObjects.length; i++) {
    if (asyncObjects[i].prototype instanceof PageAsyncObject) {
      Object.setPrototypeOf(asyncObjects[i].prototype, PageAsyncObject.prototype);
      Object.setPrototypeOf(asyncObjects[i], PageAsyncObject);
    }
  }
}

transformAsyncObjects(AsyncObject1, AsyncObject2, AsyncObject3)

Also, it's possible to create variation of library from @cuties for @page-libs.

page-ajax (based on page-cutie)

This library allows you to use ajax in very conviniet way. It works like external request objects in cutie-http or cutie-https.

Example

new ResponseBody(
  new ResponseFromAjaxRequest({
    url: 'http://localhost:8000/html/res.html',
    method: 'GET'
  })
).call()

So, as you can see here, it's possible wrap async request with just one object using Async Tree Pattern.

page-unit

This library allows you to represent html code as js code, so you can encapsulate entire behaviour of DOM elements in objects.

Example

We can represent following html code:

<div id="user-form">
  <input id ="name">
  <input id ="password">
  <button id="submit">Sign in</button>
</div>

like this composition:

new UserForm(
  document.getElementById('user-form'), 
  new NameInput(document.getElementById('name')),
  new PasswordInput(document.getElementById('password')),
  new SubmitButton(document.getElementById('submit'))
)

Full example here.

page-static-generator (based on cutie)

This library is simple tool for generating html pages from different templates combining them.

Example

We can build html page from these two templates:

<!-- outer.html -->
<div class="outer">
   {{ text }}
  <div class="place-for-inner-template">
    {{ innerTemplate }}
  </div>
</div>
<!-- inner.html -->
<div class="inner">
   {{ text }}
</div>

using following composition:

const { SavedPage, PrettyPage, Page, Head, Body, Script, Style, TemplateWithParams, Template } = require('@page-libs/static-generator')

new SavedPage(
  'page.html', 
  new PrettyPage(
    new Page(
      'xmlns="http://www.w3.org/1999/xhtml" lang="en"',
      new Head(
        new Script('script1.js', 'type="text/javascript"'),
        new Script('script2.js', 'type="text/javascript"'),
        new Style('main.css', 'type="text/css"'),
        new Style('mobile.css', 'type="text/css"')
      ),
      new Body(
        'class="main"',
        new TemplateWithParams(
          new Template('outer.html'),
          'text in outer template',
          new TemplateWithParams(
            new Template('inner.html'),
            'text in inner template'
          )
        )
      )
    )
  )
).call()

The result is

<!-- page.html -->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">

  <head>
    <script src="script1.js" type="text/javascript"></script>
    <script src="script2.js" type="text/javascript"></script>
    <link rel="stylesheet" href="main.css" type="text/css">
    <link rel="stylesheet" href="mobile.css" type="text/css">
  </head>

  <body class="main">
    <div class="outer">
      text in outer template
      <div class="place-for-inner-template">
        <div class="inner">
          text in inner template
        </div>

      </div>
    </div>
  </body>

</html>

You can find more information about usage here.

page-md2html (based on cutie)

This library is simple tool for transforming text from markdown to the html. Based on this lib.

Example

new HtmlFromMd(markdownText).call();

page-dom (based on page-cutie)

This library is set of async objects for creating DOM elements.

Example

This simple example just shows the way of declaration in this lib:

new ElementWithAppendedChildren(
  document.createElement('div'),
  div('class="div" id="div1"')(
    h1()(), 
    a('href="guseyn.com"')(),
    div('class="div" id="div2"')(
      img('src="image.png"')(),
      p('', 'text')()
    )
  )
).call()

Here you can find more examples.

Common Sense

I don't think that it makes sense to wrap every static function in browser js with objects. In my opinion, it's overhead. It's better to use page-unit with procedural code in events(like in onclick methods). Actually, procedural code is clear, if amount of it is not big. Probably some heavy stuff will be written in OOP style in other libs for Page, but for simple things it's better use good old procedural js.

However, this example is not the case, obviously. I think that creating element in DOM in declarative style is much better than the same code in procedural style.

Build System

Page uses grunt. If you look at Gruntfile.js, you'll see 3 tasks: babel, browserify, uglify.

babel

We recommend to write code in es6 style and keep your code in static/js/es6. Babel takes this code, convert it into JavaScript that browsers can understand and put it in static/js/out.

browserify

We need this step because browsers don't understand require(). Using browserify we make module system work in browser, so use also can use node_modules in your project. It generates static\js\bundle.js.

uglify

It just minifies static\js\bundle.js into static\js\bundle.min.js.

It's Your Choice

Obviously, you can choose any other build system for your browser js code you want. It's just an example of how it could be.

Future plans

Just to create more little useful libs for Page.

You can’t perform that action at this time.