├── .babelrc
├── .eslintrc
├── .github
└── workflows
│ ├── node-aught.yml
│ ├── node-pretest.yml
│ ├── node-tens.yml
│ ├── rebase.yml
│ └── require-allow-edits.yml
├── .gitignore
├── .npmignore
├── .npmrc
├── .nycrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── docs
├── client-spec.md
├── clients.md
├── hypernova-how-it-works.png
└── markup.md
├── examples
└── simple
│ ├── .gitignore
│ ├── Gemfile
│ ├── Gemfile.lock
│ ├── README.md
│ ├── README.rdoc
│ ├── Rakefile
│ ├── app
│ ├── assets
│ │ ├── images
│ │ │ └── .keep
│ │ ├── javascripts
│ │ │ ├── MyComponent.js
│ │ │ ├── application.js
│ │ │ └── welcome.coffee
│ │ └── stylesheets
│ │ │ ├── application.css
│ │ │ └── welcome.scss
│ ├── controllers
│ │ ├── application_controller.rb
│ │ ├── concerns
│ │ │ └── .keep
│ │ └── welcome_controller.rb
│ ├── helpers
│ │ ├── application_helper.rb
│ │ └── welcome_helper.rb
│ ├── mailers
│ │ └── .keep
│ ├── models
│ │ ├── .keep
│ │ └── concerns
│ │ │ └── .keep
│ └── views
│ │ ├── layouts
│ │ └── application.html.erb
│ │ └── welcome
│ │ └── index.html.erb
│ ├── bin
│ ├── bundle
│ ├── rails
│ ├── rake
│ ├── setup
│ └── spring
│ ├── config.ru
│ ├── config
│ ├── application.rb
│ ├── boot.rb
│ ├── database.yml
│ ├── environment.rb
│ ├── environments
│ │ ├── development.rb
│ │ ├── production.rb
│ │ └── test.rb
│ ├── initializers
│ │ ├── assets.rb
│ │ ├── backtrace_silencers.rb
│ │ ├── cookies_serializer.rb
│ │ ├── filter_parameter_logging.rb
│ │ ├── hypernova.rb
│ │ ├── inflections.rb
│ │ ├── mime_types.rb
│ │ ├── session_store.rb
│ │ └── wrap_parameters.rb
│ ├── locales
│ │ └── en.yml
│ ├── routes.rb
│ └── secrets.yml
│ ├── db
│ └── seeds.rb
│ ├── hypernova.js
│ ├── log
│ └── .keep
│ ├── package.json
│ ├── public
│ ├── 404.html
│ ├── 422.html
│ ├── 500.html
│ ├── favicon.ico
│ └── robots.txt
│ ├── test
│ ├── controllers
│ │ ├── .keep
│ │ └── welcome_controller_test.rb
│ ├── fixtures
│ │ └── .keep
│ ├── helpers
│ │ └── .keep
│ ├── integration
│ │ └── .keep
│ ├── mailers
│ │ └── .keep
│ ├── models
│ │ └── .keep
│ └── test_helper.rb
│ └── vendor
│ └── assets
│ ├── javascripts
│ └── .keep
│ └── stylesheets
│ └── .keep
├── package.json
├── server.js
├── src
├── Module.js
├── coordinator.js
├── createGetComponent.js
├── createVM.js
├── environment.js
├── getFiles.js
├── index.js
├── loadModules.js
├── server.js
├── utils
│ ├── BatchManager.js
│ ├── lifecycle.js
│ ├── logger.js
│ └── renderBatch.js
└── worker.js
└── test
├── BatchManager-test.js
├── Module-test.js
├── a.js
├── b.js
├── checkIso.js
├── client-test.js
├── components
├── HypernovaExample.js
└── nested
│ └── component.bundle.js
├── coordinator-test.js
├── createGetComponent-test.js
├── createVM-test.js
├── escape-test.js
├── getFiles-test.js
├── helper.js
├── hypernova-runner-test.js
├── index-test.js
├── init.js
├── lifecycle-test.js
├── loadModules-test.js
├── mutableArray.js
├── renderBatch-test.js
└── server-test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["airbnb"],
3 | "plugins": [
4 | "add-module-exports",
5 | ["transform-replace-object-assign", { "moduleSpecifier": "object.assign" }],
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": "airbnb-base",
4 | "env": {
5 | "node": true,
6 | },
7 |
8 | "rules": {
9 | "function-paren-newline": 0,
10 | "no-console": 0,
11 | "no-underscore-dangle": 1,
12 | "object-curly-newline": 0,
13 | "max-len": 0,
14 | },
15 |
16 | "overrides": [
17 | {
18 | "files": "test/**/*",
19 | "env": {
20 | "mocha": true,
21 | },
22 | "rules": {
23 | "indent": 1,
24 | "prefer-promise-reject-errors": 0,
25 | },
26 | },
27 | {
28 | "files": "test/components/**/*",
29 | "rules": {
30 | "object-shorthand": 0,
31 | "func-names": 0,
32 | },
33 | },
34 | ],
35 | }
36 |
--------------------------------------------------------------------------------
/.github/workflows/node-aught.yml:
--------------------------------------------------------------------------------
1 | name: 'Tests: node.js < 10'
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | tests:
7 | uses: ljharb/actions/.github/workflows/node.yml@main
8 | with:
9 | range: '>= 4 < 10' # node < 4 breaks due to babel; we'll need staged builds for this
10 | type: majors
11 | command: npm run tests-only
12 | skip-ls-check: true
13 |
14 | node:
15 | name: 'node < 10'
16 | needs: [tests]
17 | runs-on: ubuntu-latest
18 | steps:
19 | - run: 'echo tests completed'
20 |
--------------------------------------------------------------------------------
/.github/workflows/node-pretest.yml:
--------------------------------------------------------------------------------
1 | name: 'Tests: pretest/posttest'
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | tests:
7 | uses: ljharb/actions/.github/workflows/pretest.yml@main
8 |
--------------------------------------------------------------------------------
/.github/workflows/node-tens.yml:
--------------------------------------------------------------------------------
1 | name: 'Tests: node.js >= 10'
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | tests:
7 | uses: ljharb/actions/.github/workflows/node.yml@main
8 | with:
9 | range: '>= 10'
10 | type: majors
11 | command: npm run tests-only
12 |
13 | node:
14 | name: 'node >= 10'
15 | needs: [tests]
16 | runs-on: ubuntu-latest
17 | steps:
18 | - run: 'echo tests completed'
19 |
--------------------------------------------------------------------------------
/.github/workflows/rebase.yml:
--------------------------------------------------------------------------------
1 | name: Automatic Rebase
2 |
3 | on: [pull_request_target]
4 |
5 | jobs:
6 | _:
7 | name: "Automatic Rebase"
8 |
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: ljharb/rebase@master
14 | env:
15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
16 |
--------------------------------------------------------------------------------
/.github/workflows/require-allow-edits.yml:
--------------------------------------------------------------------------------
1 | name: Require “Allow Edits”
2 |
3 | on: [pull_request_target]
4 |
5 | jobs:
6 | _:
7 | name: "Require “Allow Edits”"
8 |
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: ljharb/require-allow-edits@main
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
35 | TODO
36 |
37 | # coverage
38 | .nyc_output
39 | coverage
40 |
41 | # build output
42 | lib
43 |
44 | # Only apps should have lockfiles
45 | yarn.lock
46 | npm-shrinkwrap.json
47 | package-lock.json
48 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
35 | TODO
36 |
37 | # coverage
38 | .nyc_output
39 | coverage
40 |
41 | examples
42 | docs
43 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 | audit-level=critical
3 |
--------------------------------------------------------------------------------
/.nycrc:
--------------------------------------------------------------------------------
1 | {
2 | "all": true,
3 | "check-coverage": false,
4 | "reporter": ["text-summary", "text", "html", "json"],
5 | "lines": 86,
6 | "statements": 85.93,
7 | "functions": 82.43,
8 | "branches": 76.06,
9 | "exclude": [
10 | "coverage",
11 | "test"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 | This project adheres to [Semantic Versioning](http://semver.org/).
5 |
6 | ## [2.5.0] - 2019-01-02
7 |
8 | ### Added
9 | - worker: Add graceful shutdown (#147, #148)
10 |
11 | ## [2.4.0] - 2018-08-04
12 |
13 | ### Added
14 | - Add option to pass an express instance in the configuration (#132)
15 |
16 | ### Docs
17 | - Update README.md: correctly use curly quotation marks (#143)
18 |
19 | ## [2.3.0] - 2018-08-04
20 |
21 | ### Added
22 | - [deps] allow `airbnb-js-shims v2 or v3
23 |
24 | ## [2.2.6] - 2018-05-10
25 |
26 | ### Added
27 |
28 | - Allow logger instance to be injected
29 |
30 | ## [2.2.5] - 2018-04-05
31 |
32 | ### Added
33 |
34 | - Handle timeout in coordinator shutdown to kill workers that have not shut down.
35 |
36 | ## [2.2.4] - 2018-03-20
37 |
38 | ### Changed
39 |
40 | - Refactor server/worker configuration into smaller pieces to be exported
41 |
42 | ## [2.2.3] - 2018-03-01
43 |
44 | ### Changed
45 |
46 | - Clear timeout set in raceTo
47 |
48 | ## [2.2.2] - 2018-02-26
49 |
50 | ### Added
51 |
52 | - Option to execute jobs in a batch serially, rather than concurrently
53 |
54 | ## [2.2.1] - 2018-02-26
55 |
56 | Bit of a flub with dist-tags, skipped version
57 |
58 | ## [2.2.0] - 2017-10-06
59 |
60 | ### Changed
61 |
62 | - If no HTML is returned from the render function then Hypernova will reject the Promise.
63 |
64 | ## [2.1.3] - 2017-06-16
65 |
66 | ### Added
67 |
68 | - Number of CPUs is now configurable.
69 | - Host is configurable.
70 |
71 |
72 | ## [2.1.1] - 2017-06-15
73 |
74 | ### Changed
75 |
76 | - You may now return a Promise from the top-level render function.
77 |
78 |
79 | ## [2.0.0] - 2016-09-15
80 |
81 | ### Breaking Changes
82 |
83 | - `toScript` function signature changed. It now expects an object of data attributes to value.
84 |
85 | ```js
86 | // before
87 | toScript('foo', 'bar', { hello: 'world' })
88 |
89 | // now
90 | toScript({ foo: 'bar' }, { hello: 'world' })
91 | ```
92 |
93 | - `fromScript` function signature changed.
94 |
95 | ```js
96 | // before
97 | fromScript('foo', 'bar')
98 |
99 | // now
100 | fromScript({ foo: 'bar' })
101 | ```
102 |
103 | ## [1.2.0] - 2016-09-08
104 |
105 | ### Changed
106 |
107 | - Exceptions that are not Errors are no longer wrapped in an Error so the stack trace does not
108 | include the Hypernova callsite.
109 |
110 | ### Added
111 |
112 | - Passing in `context` into `getComponent` which contains things like the `props` that the
113 | component will receive.
114 |
115 | ## [1.1.0] - 2016-06-15
116 |
117 | ### Changed
118 |
119 | - Documentation fixes.
120 | - Allows non-errors to be rejected from Promises in getComponent.
121 | - Sets worker count to 1 when cpu count is 1.
122 | - Makes the endpoint configurable.
123 | - Exports worker functions so you can customize your own worker.
124 |
125 | ## [1.0.0] - 2016-06-06
126 |
127 | Initial Release
128 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Airbnb
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | | :exclamation: Deprecation Notice |
2 | |:-|
3 | |We want to express our sincere gratitude for your support and contributions to the Hypernova open source project. As we are no longer using this technology internally, we have come to the decision to archive the Hypernova repositories. While we won't be providing further updates or support, the existing code and resources will remain accessible for your reference. We encourage anyone interested to fork the repository and continue the project's legacy independently. Thank you for being a part of this journey and for your patience and understanding.|
4 | ---
5 |
6 | # Hypernova
7 |
8 | > A service for server-side rendering your JavaScript views
9 |
10 | [](https://gitter.im/airbnb/hypernova?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
11 |
12 | [](http://badge.fury.io/js/hypernova)
13 | [](http://travis-ci.org/airbnb/hypernova)
14 | [](https://david-dm.org/airbnb/hypernova)
15 |
16 | ## Why?
17 |
18 | First and foremost, server-side rendering is a better user experience compared to just client-side rendering. The user gets the content faster, the webpage is more accessible when JS fails or is disabled, and search engines have an easier time indexing it.
19 |
20 | Secondly, it provides a better developer experience. Writing the same markup twice both on the server in your preferred templating library and in JavaScript can be tedious and hard to maintain. Hypernova lets you write all of your view code in a single place without having to sacrifice the user’s experience.
21 |
22 | ## How?
23 |
24 | 
25 |
26 | 1. A user requests a page on your server.
27 | 1. Your server then gathers all the data it needs to render the page.
28 | 1. Your server uses a Hypernova client to submit an HTTP request to a Hypernova server.
29 | 1. Hypernova server computes all the views into an HTML string and sends them back to the client.
30 | 1. Your server then sends down the markup plus the JavaScript to the browser.
31 | 1. On the browser, JavaScript is used to progressively enhance the application and make it dynamic.
32 |
33 | ## Terminology
34 |
35 | * **hypernova/server** - Service that accepts data via HTTP request and responds with HTML.
36 | * **hypernova** - The universal component that takes care of turning your view into the HTML structure it needs to server-render. On the browser it bootstraps the server-rendered markup and runs it.
37 | * **hypernova-${client}** - This can be something like `hypernova-ruby` or `hypernova-node`. It is the client which gives your application the superpower of querying Hypernova and understanding how to fallback to client-rendering in case there is a failure.
38 |
39 | ## Get Started
40 |
41 | First you’ll need to install a few packages: the server, the browser component, and the client. For development purposes it is recommended to install either alongside the code you wish to server-render or in the same application.
42 |
43 | From here on out we’ll assume you’re using [`hypernova-ruby`](https://github.com/airbnb/hypernova-ruby) and `React` with [`hypernova-react`](https://github.com/airbnb/hypernova-react).
44 |
45 | ### Node
46 |
47 | ```sh
48 | npm install hypernova --save
49 | ```
50 |
51 | This package contains both the server and the client.
52 |
53 | Next, lets configure the development server. To keep things simple we can put the configuration in your root folder, it can be named something like `hypernova.js`.
54 |
55 | ```js
56 | var hypernova = require('hypernova/server');
57 |
58 | hypernova({
59 | devMode: true,
60 |
61 | getComponent(name) {
62 | if (name === 'MyComponent.js') {
63 | return require('./app/assets/javascripts/MyComponent.js');
64 | }
65 | return null;
66 | },
67 |
68 | port: 3030,
69 | });
70 | ```
71 |
72 | Only the `getComponent` function is required for Hypernova. All other configuration options are optional. [Notes on `getComponent` can be found below](#getcomponent).
73 |
74 | We can run this server by starting it up with node.
75 |
76 | ```sh
77 | node hypernova.js
78 | ```
79 |
80 | If all goes well you should see a message that says "Connected". If there is an issue, a stack trace should appear in `stderr`.
81 |
82 | ### Rails
83 |
84 | If your server code is written in a language other than Ruby, then you can build your own client for Hypernova. A [spec](docs/client-spec.md) exists and details on how clients should function as well as fall-back in case of failure.
85 |
86 | Add this line to your application’s Gemfile:
87 |
88 | ```ruby
89 | gem 'hypernova'
90 | ```
91 |
92 | And then execute:
93 |
94 | $ bundle
95 |
96 | Or install it yourself as:
97 |
98 | $ gem install hypernova
99 |
100 | Now lets add support on the Rails side for Hypernova. First, we’ll need to create an initializer.
101 |
102 | `config/initializers/hypernova_initializer.rb`
103 |
104 | ```ruby
105 | Hypernova.configure do |config|
106 | config.host = "localhost"
107 | config.port = 3030 # The port where the node service is listening
108 | end
109 | ```
110 |
111 | In your controller, you’ll need an `:around_filter` so you can opt into Hypernova rendering of view partials.
112 |
113 | ```ruby
114 | class SampleController < ApplicationController
115 | around_filter :hypernova_render_support
116 | end
117 | ```
118 |
119 | And then in your view we `render_react_component`.
120 |
121 | ```ruby
122 | <%= render_react_component('MyComponent.js', :name => 'Hypernova The Renderer') %>
123 | ```
124 |
125 | ### JavaScript
126 |
127 | Finally, lets set up `MyComponent.js` to be server-rendered. We will be using React to render.
128 |
129 | ```js
130 | const React = require('react');
131 | const renderReact = require('hypernova-react').renderReact;
132 |
133 | function MyComponent(props) {
134 | return
Hello, {props.name}!
;
135 | }
136 |
137 | module.exports = renderReact('MyComponent.js', MyComponent);
138 | ```
139 |
140 | Visit the page and you should see your React component has been server-rendered. If you’d like to confirm, you can view the source of the page and look for `data-hypernova-key`. If you see a `div` filled with HTML then your component was server-rendered, if the `div` is empty then there was a problem and your component was client-rendered as a fall-back strategy.
141 |
142 | If the `div` was empty, you can check `stderr` where you’re running the node service.
143 |
144 | ## Debugging
145 |
146 | The [developer plugin](https://github.com/airbnb/hypernova-ruby/blob/master/lib/hypernova/plugins/development_mode_plugin.rb) for [`hypernova-ruby`](https://github.com/airbnb/hypernova-ruby) is useful for debugging issues with Hypernova and why it falls back to client-rendering. It’ll display a warning plus a stack trace on the page whenever a component fails to render server-side.
147 |
148 | You can install the developer plugin in `examples/simple/config/environments/development.rb`
149 |
150 | ```ruby
151 | require 'hypernova'
152 | require 'hypernova/plugins/development_mode_plugin'
153 |
154 | Hypernova.add_plugin!(DevelopmentModePlugin.new)
155 | ```
156 |
157 | You can also check the output of the server. The server outputs to `stdout` and `stderr` so if there is an error, check the process where you ran `node hypernova.js` and you should see the error.
158 |
159 | ## Deploying
160 |
161 | The recommended approach is running two separate servers, one that contains your server code and another that contains the Hypernova service. You’ll need to deploy the JavaScript code to the server that contains the Hypernova service as well.
162 |
163 | Depending on how you have `getComponent` configured, you might need to restart your Hypernova service on every deploy. If `getComponent` caches any code then a restart is paramount so that Hypernova receives the new changes. Caching is recommended because it helps speed up the service.
164 |
165 | ## FAQ
166 |
167 | > Isn’t sending an HTTP request slow?
168 |
169 | There isn’t a lot of overhead or latency, especially if you keep the servers in close proximity to each other. It’s as fast as compiling many ERB templates and gives you the benefit of unifying your view code.
170 |
171 | > Why not an in-memory JS VM?
172 |
173 | This is a valid option. If you’re looking for a siloed experience where the JS service is kept separate, then Hypernova is right for you. This approach also lends itself better to environments that don’t already have a JS VM available.
174 |
175 | > What if the server blows up?
176 |
177 | If something bad happens while Hypernova is attempting to server-render your components it’ll default to failure mode where your page will be client-rendered instead. While this is a comfortable safety net, the goal is to server-render every request.
178 |
179 | ## Pitfalls
180 |
181 | These are pitfalls of server-rendering JavaScript code and are not specific to Hypernova.
182 |
183 | * You’ll want to do any DOM-related manipulations in `componentDidMount`. `componentDidMount` runs
184 | on the browser but not the server, which means it’s safe to put DOM logic in there.
185 | Putting logic outside of the component, in the constructor, or in `componentWillMount` will
186 | cause the code to fail since the DOM isn’t present on the server.
187 |
188 | * It is recommended that you run your code in a VM sandbox so that requests get a fresh new
189 | JavaScript environment. In the event that you decide not to use a VM, you should be aware that
190 | singleton patterns and globals run the risk of leaking memory and/or leaking data
191 | between requests. If you use `createGetComponent` you’ll get VM by default.
192 |
193 | ## Clients
194 |
195 | See [clients.md](docs/clients.md)
196 |
197 | ## Browser
198 |
199 | The included browser package is a barebones helper which renders markup on the server and then loads it on the browser.
200 |
201 | List of compatible browser packages:
202 |
203 | * [`hypernova-react`](https://github.com/airbnb/hypernova-react)
204 | * [`hypernova-aphrodite`](https://github.com/airbnb/hypernova-aphrodite)
205 | * [`hypernova-styled-components`](https://github.com/viatsko/hypernova-styled-components)
206 |
207 | ## Server
208 |
209 | Starting up a Hypernova server
210 |
211 | ```js
212 | const hypernova = require('hypernova/server');
213 |
214 | hypernova({
215 | getComponent: require,
216 | });
217 | ```
218 |
219 | Options, and their defaults
220 |
221 | ```js
222 | {
223 | // the limit at which body parser will throw
224 | bodyParser: {
225 | limit: 1024 * 1000,
226 | },
227 | // runs on a single process
228 | devMode: false,
229 | // how components will be retrieved,
230 | getComponent: undefined,
231 | // if not overridden, default will return the number of reported cpus - 1
232 | getCPUs: undefined,
233 | // the host the app will bind to
234 | host: '0.0.0.0',
235 | // configure the default winston logger
236 | logger: {},
237 | // logger instance to use instead of the default winston logger
238 | loggerInstance: undefined,
239 | // the port the app will start on
240 | port: 8080,
241 | // default endpoint path
242 | endpoint: '/batch',
243 | // whether jobs in a batch are processed concurrently
244 | processJobsConcurrently: true,
245 | // arguments for server.listen, by default set to the configured [port, host]
246 | listenArgs: null,
247 | // default function to create an express app
248 | createApplication: () => express()
249 | }
250 | ```
251 |
252 | #### `getComponent`
253 |
254 | This lets you provide your own implementation on how components are retrieved.
255 |
256 | The most common use-case would be to use a VM to keep each module sandboxed between requests. You can use `createGetComponent` from Hypernova to retrieve a `getComponent` function that does this.
257 |
258 | `createGetComponent` receives an Object whose keys are the component’s registered name and the value is the absolute path to the component.
259 |
260 | ```js
261 | const path = require('path');
262 |
263 | hypernova({
264 | getComponent: createGetComponent({
265 | MyComponent: path.resolve(path.join('app', 'assets', 'javascripts', 'MyComponent.js')),
266 | }),
267 | });
268 | ```
269 |
270 | The simplest `getComponent` would be to use `require`. One drawback here is that your components would be cached between requests and thus could leak memory and/or data. Another drawback is that the files would have to exist relative to where this require is being used.
271 |
272 | ```js
273 | hypernova({
274 | getComponent: require,
275 | });
276 | ```
277 |
278 | You can also fetch components asynchronously if you wish, and/or cache them. Just return a `Promise` from `getComponent`.
279 |
280 | ```js
281 | hypernova({
282 | getComponent(name) {
283 | return promiseFetch('https://MyComponent');
284 | },
285 | });
286 | ```
287 |
288 | #### `getCPUs`
289 |
290 | This lets you specify the number of cores Hypernova will run workers on. Receives an argument containing the number of cores as reported by the OS.
291 |
292 | If this method is not overridden, or if a falsy value is passed, the default method will return the number of reported cores minus 1.
293 |
294 | #### `loggerInstance`
295 | This lets you provide your own implementation of a logger as long as it has a `log()` method.
296 |
297 | ```js
298 | const winston = require('winston');
299 | const options = {};
300 |
301 | hypernova({
302 | loggerInstance: new winston.Logger({
303 | transports: [
304 | new winston.transports.Console(options),
305 | ],
306 | }),
307 | });
308 | ```
309 |
310 | #### `processJobsConcurrently`
311 |
312 | This determines whether jobs in a batch are processed concurrently or serially. Serial execution is preferable if you use a renderer that is CPU bound and your plugins do not perform IO in the per job hooks.
313 |
314 | #### `createApplication`
315 | This lets you provide your own function that creates an express app.
316 | You are able to add your own express stuff like more routes, middlewares, etc.
317 | Notice that you __must__ pass a function that returns an express app without calling the `listen` method!
318 |
319 | ```js
320 | const express = require('express');
321 | const yourOwnAwesomeMiddleware = require('custom-middleware');
322 |
323 | hypernova({
324 | createApplication: function() {
325 | const app = express();
326 | app.use(yourOwnAwesomeMiddleware);
327 |
328 | app.get('/health', function(req, res) {
329 | return res.status(200).send('OK');
330 | });
331 |
332 | // this is mandatory.
333 | return app;
334 | }
335 | ```
336 |
337 | ## API
338 |
339 | ### Browser
340 |
341 | #### load
342 |
343 | ```typescript
344 | type DeserializedData = { [x: string]: any };
345 | type ServerRenderedPair = { node: HTMLElement, data: DeserializedData };
346 |
347 | function load(name: string): Array {}
348 | ```
349 |
350 | Looks up the server-rendered DOM markup and its corresponding `script` JSON payload and returns it.
351 |
352 | #### serialize
353 |
354 | ```typescript
355 | type DeserializedData = { [x: string]: any };
356 |
357 | function serialize(name: string, html: string, data: DeserializedData): string {}
358 | ```
359 |
360 | Generates the markup that the browser will need to bootstrap your view on the browser.
361 |
362 | #### toScript
363 |
364 | ```typescript
365 | type DeserializedData = { [x: string]: any };
366 | type Attributes = { [x: string]: string };
367 |
368 | function toScript(attrs: Attributes, props: DeserializedData): string {}
369 | ```
370 |
371 | An interface that allows you to create extra `script` tags for loading more data on the browser.
372 |
373 | #### fromScript
374 |
375 | ```typescript
376 | type DeserializedData = { [x: string]: any };
377 | type Attributes = { [x: string]: string };
378 |
379 | function fromScript(attrs: Attributes): DeserializedData {}
380 | ```
381 |
382 | The inverse of `toScript`, this function runs on the browser and attempts to find and `JSON.parse` the contents of the server generated script.
383 | `attrs` is an object where the key will be a `data-key` to be placed on the element, and the value is the data attribute's value.
384 |
385 | The `serialize` function uses the attributes `DATA_KEY` and `DATA_ID` to generate the data markup. They can be used in the `fromScript` function to get the serialized data.
386 |
387 | ```typescript
388 | import { DATA_KEY, DATA_ID } from 'hypernova'
389 |
390 | fromScript({
391 | [DATA_KEY]: key,
392 | [DATA_ID]: id,
393 | });
394 | ```
395 |
396 | ### Server
397 |
398 | #### [createGetComponent](src/createGetComponent.js)
399 |
400 | ```typescript
401 | type Files = { [key: string]: string };
402 | type VMOptions = { cacheSize: number, environment?: () => any };
403 | type GetComponent = (name: string) => any;
404 |
405 | function createGetComponent(files: Files, vmOptions: VMOptions): GetComponent {}
406 | ```
407 |
408 | Creates a `getComponent` function which can then be passed into Hypernova so it knows how to retrieve your components. `createGetComponent` will create a VM so all your bundles can run independently from each other on each request so they don’t interfere with global state. Each component is also cached at startup in order to help speed up run time. The files Object key is the component’s name and its value is the absolute path to the component.
409 |
410 | #### [createVM](src/createVM.js)
411 |
412 | ```typescript
413 | type VMOptions = { cacheSize: number, environment?: () => any };
414 | type Run = (name: string, code: string) => any;
415 | type VMContainer = { exportsCache: any, run: Run };
416 |
417 | function createVM(options: VMOptions): VMContainer {}
418 | ```
419 |
420 | Creates a VM using Node’s [`vm`](https://nodejs.org/api/vm.html) module. Calling `run` will run the provided code and return its `module.exports`. `exportsCache` is an instance of [`lru-cache`](https://github.com/isaacs/node-lru-cache).
421 |
422 | #### [getFiles](src/getFiles.js)
423 |
424 | ```typescript
425 | function getFiles(fullPathStr: string): Array<{name: string, path: string}> {}
426 | ```
427 |
428 | A utility function that allows you to retrieve all JS files recursively given an absolute path.
429 |
430 | #### [Module](src/Module.js)
431 |
432 | `Module` is a class that mimics Node’s [`module`](https://github.com/nodejs/node/blob/master/lib/module.js) interface. It makes `require` relative to whatever directory it’s run against and makes sure that each JavaScript module runs in its own clean sandbox.
433 |
434 | #### [loadModules](src/loadModules.js)
435 |
436 | ```typescript
437 | function loadModules(require: any, files: Array): () => Module? {}
438 | ```
439 |
440 | Loads all of the provided files into a `Module` that can be used as a parent `Module` inside a `VM`. This utility is useful when you need to pre-load a set of shims, shams, or JavaScript files that alter the runtime context. The `require` parameter is Node.js’ `require` function.
441 |
--------------------------------------------------------------------------------
/docs/client-spec.md:
--------------------------------------------------------------------------------
1 | ## Client Spec
2 |
3 | 1. Call `getViewData(name, data)` for every view provided.
4 | - 1.1 Use the return value as the `data` field's value for the Jobs Object.
5 | 2. Call `prepareRequest(currentJobs, originalJobs)` as a reducer.
6 | - 2.1 The return value becomes the new Jobs Object.
7 | 3. Call `shouldSendRequest(jobs)` and pass in the Jobs object.
8 | - 3.1 If `false`.
9 | * 3.1.1 Create a Response Object.
10 | * 3.1.2 The `html` attribute is the fallback client-rendered output.
11 | * 3.1.3 The `error` attribute should be `null`.
12 | 4. Call `willSendRequest(jobs)`.
13 | - 4.1 Submit the HTTP Request as a POST.
14 | 5. If any error occurs up until this point create a Response Object.
15 | - 5.1 The `error` attribute should equal the Error that was thrown.
16 | - 5.2 The `html` attribute is the fallback client-rendered output.
17 | - 5.3 Call `onError(error, jobs)`.
18 | 6. When a response is received from the server:
19 | - 6.1 Iterate over the response, if the `error` field is not null then call `onError(error, job)` per job.
20 | - 6.2 Ensure that every job has an `html` field and that it is a string. If there is no HTML then use the fallback client-rendered output.
21 | 7. Call `onSuccess(response, jobs)`.
22 | 8. Call `afterResponse(currentResponse, originalResponse)` as a reducer.
23 | 9. If an error is encountered then call `onError(error, jobs)` and assert that the fallback HTML is provided.
24 |
25 | ## Client URL and Request Information
26 |
27 | #### Constructing the URL
28 |
29 | `HOST` is whatever you're running Hypernova on `localhost` usually works if you're testing locally.
30 |
31 | `PORT` is whatever is specified when you setup Hypernova. You can see an example [here in the README](https://github.com/airbnb/hypernova#node) and there's also [this working example](https://github.com/airbnb/hypernova/blob/master/examples/simple/hypernova.js#L13).
32 |
33 | The URL is `/batch` and that's just because that's what it was named. It's the [only route](https://github.com/airbnb/hypernova/blob/master/src/worker.js#L21) that is defined for express.
34 |
35 | So the full URL would be something like `http://localhost:3030/batch`
36 |
37 | #### Posting a batch of jobs
38 |
39 | The `POST` to `/batch` should look something like:
40 |
41 | ```js
42 | {
43 | "NameOfComponent": {
44 | "name": "NameOfComponent",
45 | "data": {
46 | "theseAreProps": true,
47 | "someOtherProps": ["one", "two", "three"]
48 | },
49 | }
50 | }
51 | ```
52 |
53 | If [`getComponent`](https://github.com/airbnb/hypernova#getcomponent) returns something for `"NameOfComponent"` then you should be good to go.
54 |
55 | ## Plugin Lifecycle API
56 |
57 | ```typescript
58 | function getViewData(viewName: string, data: any): any {}
59 | ```
60 |
61 | Allows you to alter the data that a "view" will receive.
62 |
63 | ```typescript
64 | type Job = { name: string, data: any };
65 | type Jobs = { [string]: Job };
66 | function prepareRequest(currentJobs: Jobs, originalJobs: Jobs): Jobs {}
67 | ```
68 |
69 | A reducer type function that is called when preparing the request that will be sent to Hypernova. This function receives the current running jobs Object and the original jobs Object.
70 |
71 | ```typescript
72 | function shouldSendRequest(jobs: Jobs): boolean {}
73 | ```
74 |
75 | An `every` type function. If one returns `false` then the request is canceled.
76 |
77 | ```typescript
78 | function willSendRequest(jobs: Jobs): void {}
79 | ```
80 |
81 | An event type function that is called prior to a request being sent.
82 |
83 | ```typescript
84 | type Job = { name: string, data: any };
85 | type Response = {
86 | [string]: {
87 | error: ?Error,
88 | html: string,
89 | job: Job,
90 | },
91 | };
92 | function afterResponse(currentResponse: any, originalResponse: Response): any {}
93 | ```
94 |
95 | A reducer type function which receives the current response and the original response from the Hypernova service.
96 |
97 | ```typescript
98 | type Job = { name: string, data: any };
99 | type Jobs = { [string]: Job };
100 | function onSuccess(response: any, jobs: Jobs): void {}
101 | ```
102 |
103 | An event type function that is called whenever a request was successful.
104 |
105 | ```typescript
106 | type Job = { name: string, data: any };
107 | type Jobs = { [string]: Job };
108 | function onError(err: Error, jobs: Jobs): void {}
109 | ```
110 |
111 | An event type function that is called whenever any error is encountered.
112 |
--------------------------------------------------------------------------------
/docs/clients.md:
--------------------------------------------------------------------------------
1 | # Clients
2 |
3 | The following are clients in the wild for connecting to Hypernova.
4 |
5 | * [Node.js](https://github.com/airbnb/hypernova-node)
6 | * [Rails](https://github.com/airbnb/hypernova-ruby)
7 | * [PHP](https://github.com/wayfair/hypernova-php)
8 | * [Pyramid](https://github.com/Yelp/pyramid-hypernova)
9 |
--------------------------------------------------------------------------------
/docs/hypernova-how-it-works.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/docs/hypernova-how-it-works.png
--------------------------------------------------------------------------------
/docs/markup.md:
--------------------------------------------------------------------------------
1 | # Markup
2 |
3 | ## Mustache Template
4 |
5 | ```mustache
6 | {{html}}
7 |
8 | ```
9 |
--------------------------------------------------------------------------------
/examples/simple/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 | /db/*.sqlite3-journal
13 |
14 | # Ignore all logfiles and tempfiles.
15 | /log/*
16 | !/log/.keep
17 | /tmp
18 |
--------------------------------------------------------------------------------
/examples/simple/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | ruby '~> 2.3.0'
3 |
4 |
5 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
6 | gem 'rails', '4.2.7.1'
7 | # Use sqlite3 as the database for Active Record
8 | gem 'sqlite3'
9 | # Use SCSS for stylesheets
10 | gem 'sass-rails', '~> 5.0'
11 | # Use Uglifier as compressor for JavaScript assets
12 | gem 'uglifier', '>= 1.3.0'
13 | # Use CoffeeScript for .coffee assets and views
14 | gem 'coffee-rails', '~> 4.1.0'
15 | # See https://github.com/rails/execjs#readme for more supported runtimes
16 | # gem 'therubyracer', platforms: :ruby
17 |
18 | # Use jquery as the JavaScript library
19 | gem 'jquery-rails'
20 | # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
21 | gem 'turbolinks'
22 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
23 | gem 'jbuilder', '~> 2.0'
24 | # bundle exec rake doc:rails generates the API under doc/api.
25 | gem 'sdoc', '~> 0.4.0', group: :doc
26 |
27 | # Use ActiveModel has_secure_password
28 | # gem 'bcrypt', '~> 3.1.7'
29 |
30 | # Use Unicorn as the app server
31 | # gem 'unicorn'
32 |
33 | # Use Capistrano for deployment
34 | # gem 'capistrano-rails', group: :development
35 |
36 | gem "browserify-rails"
37 | gem "hypernova"
38 |
39 | group :development, :test do
40 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console
41 | gem 'byebug'
42 | end
43 |
44 | group :development do
45 | # Access an IRB console on exception pages or by using <%= console %> in views
46 | gem 'web-console', '~> 2.0'
47 |
48 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
49 | gem 'spring'
50 | end
51 |
--------------------------------------------------------------------------------
/examples/simple/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actionmailer (4.2.7.1)
5 | actionpack (= 4.2.7.1)
6 | actionview (= 4.2.7.1)
7 | activejob (= 4.2.7.1)
8 | mail (~> 2.5, >= 2.5.4)
9 | rails-dom-testing (~> 1.0, >= 1.0.5)
10 | actionpack (4.2.7.1)
11 | actionview (= 4.2.7.1)
12 | activesupport (= 4.2.7.1)
13 | rack (~> 1.6)
14 | rack-test (~> 0.6.2)
15 | rails-dom-testing (~> 1.0, >= 1.0.5)
16 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
17 | actionview (4.2.7.1)
18 | activesupport (= 4.2.7.1)
19 | builder (~> 3.1)
20 | erubis (~> 2.7.0)
21 | rails-dom-testing (~> 1.0, >= 1.0.5)
22 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
23 | activejob (4.2.7.1)
24 | activesupport (= 4.2.7.1)
25 | globalid (>= 0.3.0)
26 | activemodel (4.2.7.1)
27 | activesupport (= 4.2.7.1)
28 | builder (~> 3.1)
29 | activerecord (4.2.7.1)
30 | activemodel (= 4.2.7.1)
31 | activesupport (= 4.2.7.1)
32 | arel (~> 6.0)
33 | activesupport (4.2.7.1)
34 | i18n (~> 0.7)
35 | json (~> 1.7, >= 1.7.7)
36 | minitest (~> 5.1)
37 | thread_safe (~> 0.3, >= 0.3.4)
38 | tzinfo (~> 1.1)
39 | addressable (2.5.2)
40 | public_suffix (>= 2.0.2, < 4.0)
41 | arel (6.0.4)
42 | binding_of_caller (0.8.0)
43 | debug_inspector (>= 0.0.1)
44 | browserify-rails (4.3.0)
45 | addressable (>= 2.4.0)
46 | railties (>= 4.0.0)
47 | sprockets (>= 3.6.0)
48 | builder (3.2.3)
49 | byebug (10.0.2)
50 | coffee-rails (4.1.1)
51 | coffee-script (>= 2.2.0)
52 | railties (>= 4.0.0, < 5.1.x)
53 | coffee-script (2.4.1)
54 | coffee-script-source
55 | execjs
56 | coffee-script-source (1.12.2)
57 | concurrent-ruby (1.1.3)
58 | crass (1.0.4)
59 | debug_inspector (0.0.3)
60 | erubis (2.7.0)
61 | execjs (2.7.0)
62 | faraday (0.15.3)
63 | multipart-post (>= 1.2, < 3)
64 | ffi (1.9.25)
65 | globalid (0.4.1)
66 | activesupport (>= 4.2.0)
67 | hypernova (1.3.0)
68 | faraday (~> 0.8)
69 | i18n (0.9.5)
70 | concurrent-ruby (~> 1.0)
71 | jbuilder (2.8.0)
72 | activesupport (>= 4.2.0)
73 | multi_json (>= 1.2)
74 | jquery-rails (4.3.3)
75 | rails-dom-testing (>= 1, < 3)
76 | railties (>= 4.2.0)
77 | thor (>= 0.14, < 2.0)
78 | json (1.8.6)
79 | loofah (2.2.3)
80 | crass (~> 1.0.2)
81 | nokogiri (>= 1.5.9)
82 | mail (2.7.1)
83 | mini_mime (>= 0.1.1)
84 | mini_mime (1.0.1)
85 | mini_portile2 (2.3.0)
86 | minitest (5.11.3)
87 | multi_json (1.13.1)
88 | multipart-post (2.0.0)
89 | nokogiri (1.8.5)
90 | mini_portile2 (~> 2.3.0)
91 | public_suffix (3.0.3)
92 | rack (1.6.11)
93 | rack-test (0.6.3)
94 | rack (>= 1.0)
95 | rails (4.2.7.1)
96 | actionmailer (= 4.2.7.1)
97 | actionpack (= 4.2.7.1)
98 | actionview (= 4.2.7.1)
99 | activejob (= 4.2.7.1)
100 | activemodel (= 4.2.7.1)
101 | activerecord (= 4.2.7.1)
102 | activesupport (= 4.2.7.1)
103 | bundler (>= 1.3.0, < 2.0)
104 | railties (= 4.2.7.1)
105 | sprockets-rails
106 | rails-deprecated_sanitizer (1.0.3)
107 | activesupport (>= 4.2.0.alpha)
108 | rails-dom-testing (1.0.9)
109 | activesupport (>= 4.2.0, < 5.0)
110 | nokogiri (~> 1.6)
111 | rails-deprecated_sanitizer (>= 1.0.1)
112 | rails-html-sanitizer (1.0.4)
113 | loofah (~> 2.2, >= 2.2.2)
114 | railties (4.2.7.1)
115 | actionpack (= 4.2.7.1)
116 | activesupport (= 4.2.7.1)
117 | rake (>= 0.8.7)
118 | thor (>= 0.18.1, < 2.0)
119 | rake (12.3.1)
120 | rb-fsevent (0.10.3)
121 | rb-inotify (0.9.10)
122 | ffi (>= 0.5.0, < 2)
123 | rdoc (4.3.0)
124 | sass (3.7.2)
125 | sass-listen (~> 4.0.0)
126 | sass-listen (4.0.0)
127 | rb-fsevent (~> 0.9, >= 0.9.4)
128 | rb-inotify (~> 0.9, >= 0.9.7)
129 | sass-rails (5.0.7)
130 | railties (>= 4.0.0, < 6)
131 | sass (~> 3.1)
132 | sprockets (>= 2.8, < 4.0)
133 | sprockets-rails (>= 2.0, < 4.0)
134 | tilt (>= 1.1, < 3)
135 | sdoc (0.4.2)
136 | json (~> 1.7, >= 1.7.7)
137 | rdoc (~> 4.0)
138 | spring (2.0.2)
139 | activesupport (>= 4.2)
140 | sprockets (3.7.2)
141 | concurrent-ruby (~> 1.0)
142 | rack (> 1, < 3)
143 | sprockets-rails (3.2.1)
144 | actionpack (>= 4.0)
145 | activesupport (>= 4.0)
146 | sprockets (>= 3.0.0)
147 | sqlite3 (1.3.13)
148 | thor (0.20.3)
149 | thread_safe (0.3.6)
150 | tilt (2.0.8)
151 | turbolinks (5.2.0)
152 | turbolinks-source (~> 5.2)
153 | turbolinks-source (5.2.0)
154 | tzinfo (1.2.5)
155 | thread_safe (~> 0.1)
156 | uglifier (4.1.19)
157 | execjs (>= 0.3.0, < 3)
158 | web-console (2.3.0)
159 | activemodel (>= 4.0)
160 | binding_of_caller (>= 0.7.2)
161 | railties (>= 4.0)
162 | sprockets-rails (>= 2.0, < 4.0)
163 |
164 | PLATFORMS
165 | ruby
166 |
167 | DEPENDENCIES
168 | browserify-rails
169 | byebug
170 | coffee-rails (~> 4.1.0)
171 | hypernova
172 | jbuilder (~> 2.0)
173 | jquery-rails
174 | rails (= 4.2.7.1)
175 | sass-rails (~> 5.0)
176 | sdoc (~> 0.4.0)
177 | spring
178 | sqlite3
179 | turbolinks
180 | uglifier (>= 1.3.0)
181 | web-console (~> 2.0)
182 |
183 | RUBY VERSION
184 | ruby 2.3.8p459
185 |
186 | BUNDLED WITH
187 | 1.16.6
188 |
--------------------------------------------------------------------------------
/examples/simple/README.md:
--------------------------------------------------------------------------------
1 | > How to run this example
2 |
3 | After you've git cloned the repository and are in the `examples/simple` directory you'll need to run a few commands.
4 |
5 | ```sh
6 | bundle install
7 | npm install
8 |
9 | node hypernova.js
10 | ```
11 |
12 | ...and in a separate shell:
13 |
14 | ```sh
15 | bin/rails server
16 | ```
17 |
--------------------------------------------------------------------------------
/examples/simple/README.rdoc:
--------------------------------------------------------------------------------
1 | == README
2 |
3 | This README would normally document whatever steps are necessary to get the
4 | application up and running.
5 |
6 | Things you may want to cover:
7 |
8 | * Ruby version
9 |
10 | * System dependencies
11 |
12 | * Configuration
13 |
14 | * Database creation
15 |
16 | * Database initialization
17 |
18 | * How to run the test suite
19 |
20 | * Services (job queues, cache servers, search engines, etc.)
21 |
22 | * Deployment instructions
23 |
24 | * ...
25 |
26 |
27 | Please feel free to use a different markup language if you do not plan to run
28 | rake doc:app.
29 |
--------------------------------------------------------------------------------
/examples/simple/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/examples/simple/app/assets/images/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/app/assets/images/.keep
--------------------------------------------------------------------------------
/examples/simple/app/assets/javascripts/MyComponent.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var renderReact = require('hypernova-react').renderReact;
3 |
4 | function MyComponent(props) {
5 | return React.createElement('div', {
6 | onClick() {
7 | alert('Click handlers work.');
8 | },
9 | }, 'Hello, ' + props.name + '!');
10 | }
11 |
12 | module.exports = renderReact('MyComponent.js', MyComponent);
13 |
--------------------------------------------------------------------------------
/examples/simple/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // This is a manifest file that'll be compiled into application.js, which will include all the files
2 | // listed below.
3 | //
4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6 | //
7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8 | // compiled file.
9 | //
10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require jquery
14 | //= require jquery_ujs
15 | //= require turbolinks
16 | //= require_tree .
17 |
--------------------------------------------------------------------------------
/examples/simple/app/assets/javascripts/welcome.coffee:
--------------------------------------------------------------------------------
1 | # Place all the behaviors and hooks related to the matching controller here.
2 | # All this logic will automatically be available in application.js.
3 | # You can use CoffeeScript in this file: http://coffeescript.org/
4 |
--------------------------------------------------------------------------------
/examples/simple/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any styles
10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11 | * file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/examples/simple/app/assets/stylesheets/welcome.scss:
--------------------------------------------------------------------------------
1 | // Place all the styles related to the welcome controller here.
2 | // They will automatically be included in application.css.
3 | // You can use Sass (SCSS) here: http://sass-lang.com/
4 |
--------------------------------------------------------------------------------
/examples/simple/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | # Prevent CSRF attacks by raising an exception.
3 | # For APIs, you may want to use :null_session instead.
4 | protect_from_forgery with: :exception
5 | end
6 |
--------------------------------------------------------------------------------
/examples/simple/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/examples/simple/app/controllers/welcome_controller.rb:
--------------------------------------------------------------------------------
1 | class WelcomeController < ApplicationController
2 | around_filter :hypernova_render_support
3 | def index
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/examples/simple/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/examples/simple/app/helpers/welcome_helper.rb:
--------------------------------------------------------------------------------
1 | module WelcomeHelper
2 | end
3 |
--------------------------------------------------------------------------------
/examples/simple/app/mailers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/app/mailers/.keep
--------------------------------------------------------------------------------
/examples/simple/app/models/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/app/models/.keep
--------------------------------------------------------------------------------
/examples/simple/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/app/models/concerns/.keep
--------------------------------------------------------------------------------
/examples/simple/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Simple
5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
6 | <%= csrf_meta_tags %>
7 |
8 |
9 |
10 | <%= yield %>
11 |
12 |
13 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
14 |
15 |
--------------------------------------------------------------------------------
/examples/simple/app/views/welcome/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= render_react_component('MyComponent.js', :name => 'Hypernova') %>
2 |
--------------------------------------------------------------------------------
/examples/simple/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/examples/simple/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | APP_PATH = File.expand_path('../../config/application', __FILE__)
8 | require_relative '../config/boot'
9 | require 'rails/commands'
10 |
--------------------------------------------------------------------------------
/examples/simple/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | require_relative '../config/boot'
8 | require 'rake'
9 | Rake.application.run
10 |
--------------------------------------------------------------------------------
/examples/simple/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 |
4 | # path to your application root.
5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
6 |
7 | Dir.chdir APP_ROOT do
8 | # This script is a starting point to setup your application.
9 | # Add necessary setup steps to this file:
10 |
11 | puts "== Installing dependencies =="
12 | system "gem install bundler --conservative"
13 | system "bundle check || bundle install"
14 |
15 | # puts "\n== Copying sample files =="
16 | # unless File.exist?("config/database.yml")
17 | # system "cp config/database.yml.sample config/database.yml"
18 | # end
19 |
20 | puts "\n== Preparing database =="
21 | system "bin/rake db:setup"
22 |
23 | puts "\n== Removing old logs and tempfiles =="
24 | system "rm -f log/*"
25 | system "rm -rf tmp/cache"
26 |
27 | puts "\n== Restarting application server =="
28 | system "touch tmp/restart.txt"
29 | end
30 |
--------------------------------------------------------------------------------
/examples/simple/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # This file loads spring without using Bundler, in order to be fast.
4 | # It gets overwritten when you run the `spring binstub` command.
5 |
6 | unless defined?(Spring)
7 | require 'rubygems'
8 | require 'bundler'
9 |
10 | if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m))
11 | Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) }
12 | gem 'spring', match[1]
13 | require 'spring/binstub'
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/examples/simple/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Rails.application
5 |
--------------------------------------------------------------------------------
/examples/simple/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module Simple
10 | class Application < Rails::Application
11 | # Settings in config/environments/* take precedence over those specified here.
12 | # Application configuration should go into files in config/initializers
13 | # -- all .rb files in that directory are automatically loaded.
14 |
15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
17 | # config.time_zone = 'Central Time (US & Canada)'
18 |
19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
21 | # config.i18n.default_locale = :de
22 |
23 | # Do not swallow errors in after_commit/after_rollback callbacks.
24 | config.active_record.raise_in_transactional_callbacks = true
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/examples/simple/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
2 |
3 | require 'bundler/setup' # Set up gems listed in the Gemfile.
4 |
--------------------------------------------------------------------------------
/examples/simple/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: 5
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: db/production.sqlite3
26 |
--------------------------------------------------------------------------------
/examples/simple/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/examples/simple/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | require 'hypernova'
2 | require 'hypernova/plugins/development_mode_plugin'
3 |
4 | Rails.application.configure do
5 | # Settings specified here will take precedence over those in config/application.rb.
6 |
7 | # In the development environment your application's code is reloaded on
8 | # every request. This slows down response time but is perfect for development
9 | # since you don't have to restart the web server when you make code changes.
10 | config.cache_classes = false
11 |
12 | # Do not eager load code on boot.
13 | config.eager_load = false
14 |
15 | # Show full error reports and disable caching.
16 | config.consider_all_requests_local = true
17 | config.action_controller.perform_caching = false
18 |
19 | # Don't care if the mailer can't send.
20 | config.action_mailer.raise_delivery_errors = false
21 |
22 | # Print deprecation notices to the Rails logger.
23 | config.active_support.deprecation = :log
24 |
25 | # Raise an error on page load if there are pending migrations.
26 | config.active_record.migration_error = :page_load
27 |
28 | # Debug mode disables concatenation and preprocessing of assets.
29 | # This option may cause significant delays in view rendering with a large
30 | # number of complex assets.
31 | config.assets.debug = true
32 |
33 | # Asset digests allow you to set far-future HTTP expiration dates on all assets,
34 | # yet still be able to expire them through the digest params.
35 | config.assets.digest = true
36 |
37 | # Adds additional error checking when serving assets at runtime.
38 | # Checks for improperly declared sprockets dependencies.
39 | # Raises helpful error messages.
40 | config.assets.raise_runtime_errors = true
41 |
42 | # Raises error for missing translations
43 | # config.action_view.raise_on_missing_translations = true
44 | end
45 |
46 | Hypernova.add_plugin!(DevelopmentModePlugin.new)
47 |
--------------------------------------------------------------------------------
/examples/simple/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application
18 | # Add `rack-cache` to your Gemfile before enabling this.
19 | # For large-scale production use, consider using a caching reverse proxy like
20 | # NGINX, varnish or squid.
21 | # config.action_dispatch.rack_cache = true
22 |
23 | # Disable serving static files from the `/public` folder by default since
24 | # Apache or NGINX already handles this.
25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
26 |
27 | # Compress JavaScripts and CSS.
28 | config.assets.js_compressor = :uglifier
29 | # config.assets.css_compressor = :sass
30 |
31 | # Do not fallback to assets pipeline if a precompiled asset is missed.
32 | config.assets.compile = false
33 |
34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets,
35 | # yet still be able to expire them through the digest params.
36 | config.assets.digest = true
37 |
38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
39 |
40 | # Specifies the header that your server uses for sending files.
41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
43 |
44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
45 | # config.force_ssl = true
46 |
47 | # Use the lowest log level to ensure availability of diagnostic information
48 | # when problems arise.
49 | config.log_level = :debug
50 |
51 | # Prepend all log lines with the following tags.
52 | # config.log_tags = [ :subdomain, :uuid ]
53 |
54 | # Use a different logger for distributed setups.
55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
56 |
57 | # Use a different cache store in production.
58 | # config.cache_store = :mem_cache_store
59 |
60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
61 | # config.action_controller.asset_host = 'http://assets.example.com'
62 |
63 | # Ignore bad email addresses and do not raise email delivery errors.
64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
65 | # config.action_mailer.raise_delivery_errors = false
66 |
67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
68 | # the I18n.default_locale when a translation cannot be found).
69 | config.i18n.fallbacks = true
70 |
71 | # Send deprecation notices to registered listeners.
72 | config.active_support.deprecation = :notify
73 |
74 | # Use default logging formatter so that PID and timestamp are not suppressed.
75 | config.log_formatter = ::Logger::Formatter.new
76 |
77 | # Do not dump schema after migrations.
78 | config.active_record.dump_schema_after_migration = false
79 | end
80 |
--------------------------------------------------------------------------------
/examples/simple/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure static file server for tests with Cache-Control for performance.
16 | config.serve_static_files = true
17 | config.static_cache_control = 'public, max-age=3600'
18 |
19 | # Show full error reports and disable caching.
20 | config.consider_all_requests_local = true
21 | config.action_controller.perform_caching = false
22 |
23 | # Raise exceptions instead of rendering exception templates.
24 | config.action_dispatch.show_exceptions = false
25 |
26 | # Disable request forgery protection in test environment.
27 | config.action_controller.allow_forgery_protection = false
28 |
29 | # Tell Action Mailer not to deliver emails to the real world.
30 | # The :test delivery method accumulates sent emails in the
31 | # ActionMailer::Base.deliveries array.
32 | config.action_mailer.delivery_method = :test
33 |
34 | # Randomize the order test cases are executed.
35 | config.active_support.test_order = :random
36 |
37 | # Print deprecation notices to the stderr.
38 | config.active_support.deprecation = :stderr
39 |
40 | # Raises error for missing translations
41 | # config.action_view.raise_on_missing_translations = true
42 | end
43 |
--------------------------------------------------------------------------------
/examples/simple/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Add additional assets to the asset load path
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
11 | # Rails.application.config.assets.precompile += %w( search.js )
12 |
--------------------------------------------------------------------------------
/examples/simple/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/examples/simple/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.action_dispatch.cookies_serializer = :json
4 |
--------------------------------------------------------------------------------
/examples/simple/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/examples/simple/config/initializers/hypernova.rb:
--------------------------------------------------------------------------------
1 | require 'hypernova'
2 |
3 | Hypernova.configure do |config|
4 | config.host = 'localhost'
5 | config.port = 3030
6 | end
7 |
--------------------------------------------------------------------------------
/examples/simple/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/examples/simple/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/examples/simple/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.session_store :cookie_store, key: '_simple_session'
4 |
--------------------------------------------------------------------------------
/examples/simple/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/examples/simple/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more, please read the Rails Internationalization guide
20 | # available at http://guides.rubyonrails.org/i18n.html.
21 |
22 | en:
23 | hello: "Hello world"
24 |
--------------------------------------------------------------------------------
/examples/simple/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | root 'welcome#index'
3 |
4 | # The priority is based upon order of creation: first created -> highest priority.
5 | # See how all your routes lay out with "rake routes".
6 |
7 | # You can have the root of your site routed with "root"
8 | # root 'welcome#index'
9 |
10 | # Example of regular route:
11 | # get 'products/:id' => 'catalog#view'
12 |
13 | # Example of named route that can be invoked with purchase_url(id: product.id)
14 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase
15 |
16 | # Example resource route (maps HTTP verbs to controller actions automatically):
17 | # resources :products
18 |
19 | # Example resource route with options:
20 | # resources :products do
21 | # member do
22 | # get 'short'
23 | # post 'toggle'
24 | # end
25 | #
26 | # collection do
27 | # get 'sold'
28 | # end
29 | # end
30 |
31 | # Example resource route with sub-resources:
32 | # resources :products do
33 | # resources :comments, :sales
34 | # resource :seller
35 | # end
36 |
37 | # Example resource route with more complex sub-resources:
38 | # resources :products do
39 | # resources :comments
40 | # resources :sales do
41 | # get 'recent', on: :collection
42 | # end
43 | # end
44 |
45 | # Example resource route with concerns:
46 | # concern :toggleable do
47 | # post 'toggle'
48 | # end
49 | # resources :posts, concerns: :toggleable
50 | # resources :photos, concerns: :toggleable
51 |
52 | # Example resource route within a namespace:
53 | # namespace :admin do
54 | # # Directs /admin/products/* to Admin::ProductsController
55 | # # (app/controllers/admin/products_controller.rb)
56 | # resources :products
57 | # end
58 | end
59 |
--------------------------------------------------------------------------------
/examples/simple/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rake secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: 31f18cace0831dd8a4222653a11e77f4213d114be2dabf0f6ce52be07ad29d76b7c71b1a953fbb17fee466bc6f68e0d92ae6d6aa0340231b81aa3b7a9acc8b74
15 |
16 | test:
17 | secret_key_base: e64e0b20f4f6b74cd8b27e6efa565b8b0df8ad99ae1227c60410de67d6c8bc42fb790afe4503ee73d909af1ab9d58c4ecad50bc902f6d834f36df87f17890e04
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/examples/simple/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
7 | # Mayor.create(name: 'Emanuel', city: cities.first)
8 |
--------------------------------------------------------------------------------
/examples/simple/hypernova.js:
--------------------------------------------------------------------------------
1 | const hypernova = require('hypernova/server');
2 |
3 | hypernova({
4 | devMode: true,
5 |
6 | getComponent(name) {
7 | if (name === 'MyComponent.js') {
8 | return require('./app/assets/javascripts/MyComponent.js');
9 | }
10 | return null;
11 | },
12 |
13 | port: 3030,
14 | });
15 |
--------------------------------------------------------------------------------
/examples/simple/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/log/.keep
--------------------------------------------------------------------------------
/examples/simple/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hypernova-simple-example",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "A sample Rails application that uses Hypernova to server render.",
6 | "main": "hypernova.js",
7 | "dependencies": {
8 | "hypernova": "^1.0.0",
9 | "hypernova-react": "^1.0.0",
10 | "react": "^15.0.1",
11 | "react-dom": "^15.0.1"
12 | },
13 | "devDependencies": {
14 | "browserify": "^13.0.0",
15 | "browserify-incremental": "^3.1.1"
16 | },
17 | "author": "Josh Perez ",
18 | "license": "MIT"
19 | }
20 |
--------------------------------------------------------------------------------
/examples/simple/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/examples/simple/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/examples/simple/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/examples/simple/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/public/favicon.ico
--------------------------------------------------------------------------------
/examples/simple/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/examples/simple/test/controllers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/test/controllers/.keep
--------------------------------------------------------------------------------
/examples/simple/test/controllers/welcome_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class WelcomeControllerTest < ActionController::TestCase
4 | test "should get index" do
5 | get :index
6 | assert_response :success
7 | end
8 |
9 | end
10 |
--------------------------------------------------------------------------------
/examples/simple/test/fixtures/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/test/fixtures/.keep
--------------------------------------------------------------------------------
/examples/simple/test/helpers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/test/helpers/.keep
--------------------------------------------------------------------------------
/examples/simple/test/integration/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/test/integration/.keep
--------------------------------------------------------------------------------
/examples/simple/test/mailers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/test/mailers/.keep
--------------------------------------------------------------------------------
/examples/simple/test/models/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/test/models/.keep
--------------------------------------------------------------------------------
/examples/simple/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] ||= 'test'
2 | require File.expand_path('../../config/environment', __FILE__)
3 | require 'rails/test_help'
4 |
5 | class ActiveSupport::TestCase
6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
7 | fixtures :all
8 |
9 | # Add more helper methods to be used by all tests here...
10 | end
11 |
--------------------------------------------------------------------------------
/examples/simple/vendor/assets/javascripts/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/vendor/assets/javascripts/.keep
--------------------------------------------------------------------------------
/examples/simple/vendor/assets/stylesheets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/airbnb/hypernova/125c346022084e2d820f9da490b4468cba60d25e/examples/simple/vendor/assets/stylesheets/.keep
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hypernova",
3 | "version": "2.5.0",
4 | "description": "A service for server-side rendering your JavaScript views",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "prepublishOnly": "safe-publish-latest && npm run build",
8 | "prepublish": "not-in-publish || npm run prepublishOnly",
9 | "clean": "rimraf lib",
10 | "prebuild": "npm run clean",
11 | "build": "babel src -d lib",
12 | "prelint": "npm run build",
13 | "lint": "eslint src test",
14 | "pretest": "npm run --silent lint",
15 | "test": "npm run coverage",
16 | "pretests-only": "npm run build",
17 | "tests-only": "npm run test:quick",
18 | "posttest": "aud --production",
19 | "precoverage": "npm run build",
20 | "coverage": "babel-node node_modules/.bin/istanbul cover --report html node_modules/.bin/_mocha -- -R tap test/init.js test/*-test.js",
21 | "postcoverage": "npm run cover:check",
22 | "cover:check": "istanbul check-coverage && echo code coverage thresholds met, achievement unlocked!",
23 | "test:quick": "babel-node node_modules/.bin/_mocha -R tap test/init.js test/*-test.js"
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "git@github.com:airbnb/hypernova.git"
28 | },
29 | "keywords": [
30 | "react",
31 | "server",
32 | "render",
33 | "isomorphic",
34 | "universal",
35 | "express"
36 | ],
37 | "author": "Josh Perez ",
38 | "contributors": [
39 | "Leland Richardson ",
40 | "Jordan Harband ",
41 | "Gary Borton ",
42 | "Stephen Bush ",
43 | "Ian Myers ",
44 | "Jake Teton-Landis "
45 | ],
46 | "license": "MIT",
47 | "bugs": {
48 | "url": "https://github.com/airbnb/hypernova/issues"
49 | },
50 | "homepage": "https://github.com/airbnb/hypernova",
51 | "devDependencies": {
52 | "aud": "^2.0.0",
53 | "babel-cli": "^6.26.0",
54 | "babel-plugin-add-module-exports": "^0.2.1",
55 | "babel-plugin-transform-replace-object-assign": "^1.0.0",
56 | "babel-preset-airbnb": "^2.5.3",
57 | "chai": "^4.3.6",
58 | "cheerio": "=1.0.0-rc.3",
59 | "eslint": "^8.14.0",
60 | "eslint-config-airbnb-base": "^15.0.0",
61 | "eslint-plugin-import": "^2.26.0",
62 | "in-publish": "^2.0.1",
63 | "mocha": "^3.5.3",
64 | "mocha-wrap": "^2.1.2",
65 | "nyc": "^10.3.2",
66 | "rimraf": "^2.6.3",
67 | "safe-publish-latest": "^2.0.0",
68 | "sinon": "^3.3.0",
69 | "sinon-sandbox": "^1.0.2"
70 | },
71 | "dependencies": {
72 | "airbnb-js-shims": "^2 || ^3",
73 | "bluebird": "^3.7.2",
74 | "body-parser": "^1.20.0",
75 | "express": "^4.18.0",
76 | "glob": "^7.2.0",
77 | "has": "^1.0.3",
78 | "lru-cache": "^4.1.5",
79 | "object.assign": "^4.1.2",
80 | "winston": "^2.4.5"
81 | },
82 | "engines": {
83 | "node": ">= 0.10"
84 | },
85 | "greenkeeper": {
86 | "ignore": [
87 | "mocha",
88 | "sinon"
89 | ]
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/server.js');
2 |
--------------------------------------------------------------------------------
/src/Module.js:
--------------------------------------------------------------------------------
1 | import NativeModule from 'module';
2 | import has from 'has';
3 | import path from 'path';
4 | import { ok } from 'assert';
5 | import { runInNewContext } from 'vm';
6 |
7 | const NativeModules = process.binding('natives');
8 |
9 | // This means that you won't be able to affect VM extensions by mutating require.extensions
10 | // this is cool since we can now have different extensions for VM than for where your program is
11 | // running.
12 | // If you want to add an extension then you can use addExtension defined and exported below.
13 | const moduleExtensions = { ...NativeModule._extensions };
14 |
15 | function isNativeModule(id) {
16 | return has(NativeModules, id);
17 | }
18 |
19 | // Creates a sandbox so we don't share globals across different runs.
20 | function createContext() {
21 | const sandbox = {
22 | Buffer,
23 | clearImmediate,
24 | clearInterval,
25 | clearTimeout,
26 | setImmediate,
27 | setInterval,
28 | setTimeout,
29 | console,
30 | process,
31 | };
32 | sandbox.global = sandbox;
33 | return sandbox;
34 | }
35 |
36 | // This class should satisfy the Module interface that NodeJS defines in their native module.js
37 | // implementation.
38 | class Module {
39 | constructor(id, parent) {
40 | const cache = parent ? parent.cache : null;
41 | this.id = id;
42 | this.exports = {};
43 | this.cache = cache || {};
44 | this.parent = parent;
45 | this.filename = null;
46 | this.loaded = false;
47 | this.context = parent ? parent.context : createContext();
48 | }
49 |
50 | load(filename) {
51 | ok(!this.loaded);
52 | this.filename = filename;
53 | this.paths = NativeModule._nodeModulePaths(path.dirname(filename));
54 | }
55 |
56 | run(filename) {
57 | const ext = path.extname(filename);
58 | const extension = moduleExtensions[ext] ? ext : '.js';
59 | moduleExtensions[extension](this, filename);
60 | this.loaded = true;
61 | }
62 |
63 | require(filePath) {
64 | ok(typeof filePath === 'string', 'path must be a string');
65 | return Module.loadFile(filePath, this);
66 | }
67 |
68 | _compile(content, filename) {
69 | const self = this;
70 |
71 | function require(filePath) {
72 | return self.require(filePath);
73 | }
74 | require.resolve = (request) => NativeModule._resolveFilename(request, this);
75 | require.main = process.mainModule;
76 | require.extensions = moduleExtensions;
77 | require.cache = this.cache;
78 |
79 | const dirname = path.dirname(filename);
80 |
81 | // create wrapper function
82 | const wrapper = NativeModule.wrap(content);
83 |
84 | const options = {
85 | filename,
86 | displayErrors: true,
87 | };
88 |
89 | const compiledWrapper = runInNewContext(wrapper, this.context, options);
90 | return compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);
91 | }
92 |
93 | static load(id, filename = id) {
94 | const module = new Module(id);
95 | module.load(filename);
96 | module.run(filename);
97 | return module;
98 | }
99 |
100 | static loadFile(file, parent) {
101 | const filename = NativeModule._resolveFilename(file, parent);
102 |
103 | if (parent) {
104 | const cachedModule = parent.cache[filename];
105 | if (cachedModule) return cachedModule.exports;
106 | }
107 |
108 | if (isNativeModule(filename)) {
109 | // eslint-disable-next-line global-require, import/no-dynamic-require
110 | return require(filename);
111 | }
112 |
113 | const module = new Module(filename, parent);
114 |
115 | module.cache[filename] = module;
116 |
117 | let hadException = true;
118 |
119 | try {
120 | module.load(filename);
121 | module.run(filename);
122 | hadException = false;
123 | } finally {
124 | if (hadException) {
125 | delete module.cache[filename];
126 | }
127 | }
128 |
129 | return module.exports;
130 | }
131 |
132 | static addExtension(ext, f) {
133 | moduleExtensions[ext] = f;
134 | }
135 | }
136 |
137 | export default Module;
138 |
--------------------------------------------------------------------------------
/src/coordinator.js:
--------------------------------------------------------------------------------
1 | import cluster from 'cluster';
2 | import os from 'os';
3 |
4 | import './environment';
5 | import logger from './utils/logger';
6 | import { raceTo } from './utils/lifecycle';
7 |
8 | export function getDefaultCPUs(realCount) {
9 | if (!Number.isInteger(realCount) || realCount <= 0) {
10 | throw new TypeError('getDefaultCPUs must accept a positive integer');
11 | }
12 |
13 | return realCount - 1 || 1;
14 | }
15 |
16 | export function getWorkerCount(getCPUs = getDefaultCPUs) {
17 | const realCount = os.cpus().length;
18 |
19 | if (typeof getCPUs !== 'function') {
20 | throw new TypeError('getCPUs must be a function');
21 | }
22 |
23 | const requested = getCPUs(realCount);
24 |
25 | if (!Number.isInteger(requested) || requested <= 0) {
26 | throw new TypeError('getCPUs must return a positive integer');
27 | }
28 |
29 | return requested;
30 | }
31 |
32 | function close() {
33 | return Promise.all(Object.values(cluster.workers).map((worker) => {
34 | const promise = new Promise((resolve, reject) => {
35 | worker.once('disconnect', resolve);
36 | worker.once('exit', (code) => {
37 | if (code !== 0) reject();
38 | });
39 | });
40 | worker.send('kill');
41 | return promise;
42 | }));
43 | }
44 |
45 | function kill(signal) {
46 | const liveWorkers = Object.values(cluster.workers).filter((worker) => !worker.isDead());
47 |
48 | if (liveWorkers.length > 0) {
49 | logger.info(`Coordinator killing ${liveWorkers.length} live workers with ${signal}`);
50 |
51 | return Promise.all(liveWorkers.map((worker) => {
52 | const promise = new Promise((resolve) => {
53 | worker.once('exit', () => resolve());
54 | });
55 |
56 | worker.process.kill(signal);
57 | return promise;
58 | }));
59 | }
60 |
61 | return Promise.resolve();
62 | }
63 |
64 | function killSequence(signal) {
65 | return () => raceTo(kill(signal), 2000, `Killing workers with ${signal} took too long`);
66 | }
67 |
68 | function shutdown() {
69 | return raceTo(close(), 5000, 'Closing the coordinator took too long.')
70 | .then(killSequence('SIGTERM'), killSequence('SIGTERM'))
71 | .then(killSequence('SIGKILL'), killSequence('SIGKILL'));
72 | }
73 |
74 | function workersReady(workerCount) {
75 | const workers = Object.values(cluster.workers);
76 |
77 | return workers.length === workerCount && workers.every((worker) => worker.isReady);
78 | }
79 |
80 | export default (getCPUs) => {
81 | const workerCount = getWorkerCount(getCPUs);
82 | let closing = false;
83 |
84 | function onWorkerMessage(msg) {
85 | if (msg.ready) {
86 | cluster.workers[msg.workerId].isReady = true;
87 | }
88 |
89 | if (workersReady(workerCount)) {
90 | Object.values(cluster.workers).forEach((worker) => worker.send('healthy'));
91 | }
92 | }
93 |
94 | cluster.on('online', (worker) => logger.info(`Worker #${worker.id} is now online`));
95 |
96 | cluster.on('listening', (worker, address) => {
97 | logger.info(`Worker #${worker.id} is now connected to ${address.address}:${address.port}`);
98 | });
99 |
100 | cluster.on('disconnect', (worker) => {
101 | logger.info(`Worker #${worker.id} has disconnected`);
102 | });
103 |
104 | cluster.on('exit', (worker, code, signal) => {
105 | if (worker.exitedAfterDisconnect === true || code === 0) {
106 | logger.info(`Worker #${worker.id} shutting down.`);
107 | } else if (closing) {
108 | logger.error(
109 | `Worker #${worker.id} died with code ${signal || code} during close. Not restarting.`,
110 | );
111 | } else {
112 | logger.error(`Worker #${worker.id} died with code ${signal || code}. Restarting worker.`);
113 | const newWorker = cluster.fork();
114 | newWorker.on('message', onWorkerMessage);
115 | }
116 | });
117 |
118 | process.on('SIGTERM', () => {
119 | logger.info('Hypernova got SIGTERM. Going down.');
120 | closing = true;
121 | shutdown().then(() => process.exit(0), () => process.exit(1));
122 | });
123 |
124 | process.on('SIGINT', () => {
125 | closing = true;
126 | shutdown().then(() => process.exit(0), () => process.exit(1));
127 | });
128 |
129 | Array.from({ length: workerCount }, () => cluster.fork());
130 |
131 | Object.values(cluster.workers).forEach((worker) => worker.on('message', onWorkerMessage));
132 | };
133 |
--------------------------------------------------------------------------------
/src/createGetComponent.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import has from 'has';
3 |
4 | import createVM from './createVM';
5 |
6 | // This function takes in an Object of files and an Object that configures the VM. It will return
7 | // a function that can be used as `getComponent` for Hypernova.
8 | // The file's object structure is [componentName]: 'AbsolutePath.js'
9 | export default (files, vmOptions) => {
10 | const fileEntries = Object.entries(files);
11 |
12 | const vm = createVM({
13 | cacheSize: fileEntries.length,
14 | ...vmOptions,
15 | });
16 |
17 | const resolvedFiles = fileEntries.reduce((components, [fileName, filePath]) => {
18 | const code = fs.readFileSync(filePath, 'utf-8');
19 |
20 | try {
21 | // Load the bundle on startup so we can cache its exports.
22 | vm.run(filePath, code);
23 |
24 | // Cache the code as well as the path to it.
25 | components[fileName] = { // eslint-disable-line no-param-reassign
26 | filePath,
27 | code,
28 | };
29 | } catch (err) {
30 | // If loading the component failed then we'll skip it.
31 | // istanbul ignore next
32 | console.error(err.stack);
33 | }
34 |
35 | return components;
36 | }, {});
37 |
38 | return (name) => {
39 | if (has(resolvedFiles, name)) {
40 | const { filePath, code } = resolvedFiles[name];
41 | return vm.run(filePath, code);
42 | }
43 |
44 | // The requested package was not found.
45 | return null;
46 | };
47 | };
48 |
--------------------------------------------------------------------------------
/src/createVM.js:
--------------------------------------------------------------------------------
1 | import lruCache from 'lru-cache';
2 | import crypto from 'crypto';
3 | import Module from './Module';
4 |
5 | function defaultGetKey(name, code) {
6 | const hash = crypto.createHash('sha1').update(code).digest('hex');
7 | return `${name}::${hash}`;
8 | }
9 |
10 | export default (options = {}) => {
11 | // This is to cache the entry point of all bundles which makes running on a vm blazing fast.
12 | // Everyone gets their own sandbox to play with and nothing is leaked between requests.
13 | // We're caching with `code` as the key to ensure that if the code changes we break the cache.
14 | const exportsCache = lruCache({
15 | max: options.cacheSize,
16 | });
17 |
18 | const getKey = options.getKey || defaultGetKey;
19 |
20 | return {
21 | exportsCache,
22 |
23 | run(name, code) {
24 | const key = getKey(name, code);
25 |
26 | if (exportsCache.has(key)) return exportsCache.get(key);
27 |
28 | const environment = options.environment && options.environment(name);
29 |
30 | const module = new Module(name, environment);
31 | module.load(name);
32 | module._compile(code, name);
33 |
34 | exportsCache.set(key, module.exports);
35 |
36 | return module.exports;
37 | },
38 | };
39 | };
40 |
--------------------------------------------------------------------------------
/src/environment.js:
--------------------------------------------------------------------------------
1 | /* eslint func-names:0 no-extra-parens:0 */
2 | import 'airbnb-js-shims';
3 | import Promise from 'bluebird';
4 |
5 | const es6methods = ['then', 'catch', 'constructor'];
6 | const es6StaticMethods = ['all', 'race', 'resolve', 'reject', 'cast'];
7 |
8 | function isNotMethod(name) {
9 | return !(es6methods.includes(name) || es6StaticMethods.includes(name) || name.charAt(0) === '_');
10 | }
11 |
12 | function del(obj) {
13 | /* eslint no-param-reassign: 0 */
14 | return (key) => { delete obj[key]; };
15 | }
16 |
17 | function toFastProperties(obj) {
18 | (function () {}).prototype = obj;
19 | }
20 |
21 | Object.keys(Promise.prototype).filter(isNotMethod).forEach(del(Promise.prototype));
22 | Object.keys(Promise).filter(isNotMethod).forEach(del(Promise));
23 | toFastProperties(Promise);
24 | toFastProperties(Promise.prototype);
25 |
26 | global.Promise = Promise;
27 |
--------------------------------------------------------------------------------
/src/getFiles.js:
--------------------------------------------------------------------------------
1 | import glob from 'glob';
2 | import path from 'path';
3 |
4 | export default function getFiles(fullPathStr) {
5 | return glob.sync(path.join(fullPathStr, '**', '*.js')).map((file) => {
6 | const name = path.relative(fullPathStr, file);
7 | return {
8 | name,
9 | path: file,
10 | };
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* globals document */
2 |
3 | const LEFT = '';
5 |
6 | const ENCODE = [
7 | ['&', '&'],
8 | ['>', '>'],
9 | ];
10 |
11 | const DATA_KEY = 'hypernova-key';
12 | const DATA_ID = 'hypernova-id';
13 |
14 | // https://gist.github.com/jed/982883
15 | function uuid() {
16 | return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(
17 | /[018]/g,
18 | (x) => (x ^ Math.random() * 16 >> x / 4).toString(16), // eslint-disable-line no-mixed-operators, no-bitwise, max-len
19 | );
20 | }
21 |
22 | function encode(obj) {
23 | return ENCODE.reduce((str, coding) => {
24 | const [encodeChar, htmlEntity] = coding;
25 | return str.replace(new RegExp(encodeChar, 'g'), htmlEntity);
26 | }, JSON.stringify(obj));
27 | }
28 |
29 | function decode(res) {
30 | const jsonPayload = ENCODE.reduceRight((str, coding) => {
31 | const [encodeChar, htmlEntity] = coding;
32 | return str.replace(new RegExp(htmlEntity, 'g'), encodeChar);
33 | }, res);
34 |
35 | return JSON.parse(jsonPayload);
36 | }
37 |
38 | function makeValidDataAttribute(attr, value) {
39 | const encodedAttr = attr.toLowerCase().replace(/[^0-9a-z_-]/g, '');
40 | const encodedValue = value.replace(/&/g, '&').replace(/"/g, '"');
41 | return `data-${encodedAttr}="${encodedValue}"`;
42 | }
43 |
44 | function toScript(attrs, data) {
45 | const dataAttributes = Object.keys(attrs).map((name) => makeValidDataAttribute(name, attrs[name]));
46 | return ``; // eslint-disable-line max-len
47 | }
48 |
49 | function fromScript(attrs) {
50 | const selectors = Object.keys(attrs)
51 | .map((name) => `[${makeValidDataAttribute(name, attrs[name])}]`)
52 | .join('');
53 | const node = document.querySelector(`script${selectors}`);
54 | if (!node) return null;
55 | const jsonPayload = node.innerHTML;
56 |
57 | return decode(jsonPayload.slice(LEFT.length, jsonPayload.length - RIGHT.length));
58 | }
59 |
60 | function serialize(name, html, data) {
61 | const key = name.replace(/\W/g, '');
62 | const id = uuid();
63 | const markup = `${html}
`;
64 | const script = toScript({
65 | [DATA_KEY]: key,
66 | [DATA_ID]: id,
67 | }, data);
68 | return `${markup}\n${script}`;
69 | }
70 |
71 | function load(name) {
72 | const key = name.replace(/\W/g, '');
73 | const nodes = document.querySelectorAll(`div[data-${DATA_KEY}="${key}"]`);
74 |
75 | return Array.prototype.map.call(nodes, (node) => {
76 | const id = node.getAttribute(`data-${DATA_ID}`);
77 | const data = fromScript({
78 | [DATA_KEY]: key,
79 | [DATA_ID]: id,
80 | });
81 | return { node, data };
82 | });
83 | }
84 |
85 | export default function hypernova(runner) {
86 | return typeof window === 'undefined'
87 | ? runner.server()
88 | : runner.client();
89 | }
90 |
91 | hypernova.toScript = toScript;
92 | hypernova.fromScript = fromScript;
93 | hypernova.serialize = serialize;
94 | hypernova.load = load;
95 | hypernova.DATA_KEY = DATA_KEY;
96 | hypernova.DATA_ID = DATA_ID;
97 |
--------------------------------------------------------------------------------
/src/loadModules.js:
--------------------------------------------------------------------------------
1 | import Module from './Module';
2 |
3 | function load(file, parent) {
4 | if (!file) return parent;
5 |
6 | const module = new Module(file, parent);
7 | module.load(file);
8 | module.run(file);
9 | return module;
10 | }
11 |
12 | function resolve(require, name) {
13 | try {
14 | return require.resolve(name);
15 | } catch (e) {
16 | if (e.code === 'MODULE_NOT_FOUND') return null;
17 | throw e;
18 | }
19 | }
20 |
21 | export default function loadModules(require, files) {
22 | return () => files.reduce((module, file) => load(resolve(require, file), module), null);
23 | }
24 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | import cluster from 'cluster';
2 | import express from 'express';
3 |
4 | import './environment';
5 | import Module from './Module';
6 | import coordinator from './coordinator';
7 | import createGetComponent from './createGetComponent';
8 | import getFiles from './getFiles';
9 | import loadModules from './loadModules';
10 | import logger from './utils/logger';
11 | import createVM from './createVM';
12 | import worker from './worker';
13 | import { raceTo } from './utils/lifecycle';
14 |
15 | function createApplication() {
16 | return express();
17 | }
18 |
19 | const defaultConfig = {
20 | bodyParser: {
21 | limit: 1024 * 1000,
22 | },
23 | devMode: false,
24 | endpoint: '/batch',
25 | files: [],
26 | logger: {},
27 | plugins: [],
28 | port: 8080,
29 | host: '0.0.0.0',
30 | processJobsConcurrent: true,
31 | listenArgs: null,
32 | createApplication,
33 | };
34 |
35 | export default function hypernova(userConfig, onServer) {
36 | const config = { ...defaultConfig, ...userConfig };
37 |
38 | if (typeof config.getComponent !== 'function') {
39 | throw new TypeError('Hypernova requires a `getComponent` property and it must be a function');
40 | }
41 |
42 | if (!config.listenArgs) {
43 | config.listenArgs = [config.port, config.host];
44 | }
45 |
46 | logger.init(config.logger, config.loggerInstance);
47 |
48 | if (typeof config.createApplication !== 'function') {
49 | throw new TypeError('Hypernova requires a `createApplication` property which must be a function that returns an express instance');
50 | }
51 |
52 | const app = config.createApplication();
53 |
54 | if (
55 | typeof app !== 'function'
56 | || typeof app.use !== 'function'
57 | || typeof app.post !== 'function'
58 | || typeof app.listen !== 'function'
59 | ) {
60 | throw new TypeError(
61 | '`createApplication` must return a valid express instance with `use`, `post`, and `listen` methods',
62 | );
63 | }
64 |
65 | if (config.devMode) {
66 | worker(app, config, onServer);
67 | } else if (cluster.isMaster) {
68 | coordinator(config.getCPUs);
69 | } else {
70 | worker(app, config, onServer, cluster.worker.id);
71 | }
72 |
73 | return app;
74 | }
75 |
76 | // I'm "exporting" them here because I want to export these but still have a default export.
77 | // And I want it to work on CJS.
78 | // I want my cake and to eat it all.
79 | hypernova.Module = Module;
80 | hypernova.createApplication = createApplication;
81 | hypernova.createGetComponent = createGetComponent;
82 | hypernova.createVM = createVM;
83 | hypernova.getFiles = getFiles;
84 | hypernova.loadModules = loadModules;
85 | hypernova.worker = worker;
86 | hypernova.logger = logger;
87 | hypernova.defaultConfig = defaultConfig;
88 | hypernova.raceTo = raceTo;
89 |
--------------------------------------------------------------------------------
/src/utils/BatchManager.js:
--------------------------------------------------------------------------------
1 | const noHTMLError = new TypeError(
2 | 'HTML was not returned to Hypernova, this is most likely an error within your application. Check your logs for any uncaught errors and/or rejections.',
3 | );
4 | noHTMLError.stack = null;
5 |
6 | function errorToSerializable(error) {
7 | // istanbul ignore next
8 | if (error === undefined) throw new TypeError('No error was passed');
9 |
10 | // make sure it is an object that is Error-like so we can serialize it properly
11 | // if it's not an actual error then we won't create an Error so that there is no stack trace
12 | // because no stack trace is better than a stack trace that is generated here.
13 | const err = (
14 | Object.prototype.toString.call(error) === '[object Error]'
15 | && typeof error.stack === 'string'
16 | ) ? error : { name: 'Error', type: 'Error', message: error, stack: '' };
17 |
18 | return {
19 | type: err.type,
20 | name: err.name,
21 | message: err.message,
22 | stack: err.stack.split('\n '),
23 | };
24 | }
25 |
26 | function notFound(name) {
27 | const error = new ReferenceError(`Component "${name}" not registered`);
28 | const stack = error.stack.split('\n');
29 |
30 | error.stack = [stack[0]]
31 | .concat(
32 | ` at YOUR-COMPONENT-DID-NOT-REGISTER_${name}:1:1`,
33 | stack.slice(1),
34 | )
35 | .join('\n');
36 |
37 | return error;
38 | }
39 |
40 | function msSince(start) {
41 | const diff = process.hrtime(start);
42 | return (diff[0] * 1e3) + (diff[1] / 1e6);
43 | }
44 |
45 | function now() {
46 | return process.hrtime();
47 | }
48 |
49 | /**
50 | * The BatchManager is a class that is instantiated once per batch, and holds a lot of the
51 | * key data needed throughout the life of the request. This ends up cleaning up some of the
52 | * management needed for plugin lifecycle, and the handling of rendering multiple jobs in a
53 | * batch.
54 | *
55 | * @param {express.Request} req
56 | * @param {express.Response} res
57 | * @param {Object} jobs - a map of token => Job
58 | * @param {Object} config
59 | * @constructor
60 | */
61 | class BatchManager {
62 | constructor(request, response, jobs, config) {
63 | const tokens = Object.keys(jobs);
64 |
65 | this.config = config;
66 | this.plugins = config.plugins;
67 | this.error = null;
68 | this.statusCode = 200;
69 |
70 | // An object that all of the contexts will inherit from... one per instance.
71 | this.baseContext = {
72 | request,
73 | response,
74 | batchMeta: {},
75 | };
76 |
77 | // An object that will be passed into the context for batch-level methods, but not for job-level
78 | // methods.
79 | this.batchContext = {
80 | tokens,
81 | jobs,
82 | };
83 |
84 | // A map of token => JobContext, where JobContext is an object of data that is per-job,
85 | // and will be passed into plugins and used for the final result.
86 | this.jobContexts = tokens.reduce((obj, token) => {
87 | const { name, data, metadata } = jobs[token];
88 | /* eslint no-param-reassign: 1 */
89 | obj[token] = {
90 | name,
91 | token,
92 | props: data,
93 | metadata,
94 | statusCode: 200,
95 | duration: null,
96 | html: null,
97 | returnMeta: {},
98 | };
99 | return obj;
100 | }, {});
101 |
102 | // Each plugin receives it's own little key-value data store that is scoped privately
103 | // to the plugin for the life time of the request. This is achieved simply through lexical
104 | // closure.
105 | this.pluginContexts = new Map();
106 | this.plugins.forEach((plugin) => {
107 | this.pluginContexts.set(plugin, { data: new Map() });
108 | });
109 | }
110 |
111 | /**
112 | * Returns a context object scoped to a specific plugin and job (based on the plugin and
113 | * job token passed in).
114 | */
115 | getRequestContext(plugin, token) {
116 | return {
117 | ...this.baseContext,
118 | ...this.jobContexts[token],
119 | ...this.pluginContexts.get(plugin),
120 | };
121 | }
122 |
123 | /**
124 | * Returns a context object scoped to a specific plugin and batch.
125 | */
126 | getBatchContext(plugin) {
127 | return {
128 | ...this.baseContext,
129 | ...this.batchContext,
130 | ...this.pluginContexts.get(plugin),
131 | };
132 | }
133 |
134 | contextFor(plugin, token) {
135 | return token ? this.getRequestContext(plugin, token) : this.getBatchContext(plugin);
136 | }
137 |
138 | /**
139 | * Renders a specific job (from a job token). The end result is applied to the corresponding
140 | * job context. Additionally, duration is calculated.
141 | */
142 | render(token) {
143 | const start = now();
144 | const context = this.jobContexts[token];
145 | const { name } = context;
146 |
147 | const { getComponent } = this.config;
148 |
149 | const result = getComponent(name, context);
150 |
151 | return Promise.resolve(result).then((renderFn) => {
152 | // ensure that we have this component registered
153 | if (!renderFn || typeof renderFn !== 'function') {
154 | // component not registered
155 | context.statusCode = 404;
156 | return Promise.reject(notFound(name));
157 | }
158 |
159 | return renderFn(context.props);
160 | }).then((html) => { // eslint-disable-line consistent-return
161 | if (!html) {
162 | return Promise.reject(noHTMLError);
163 | }
164 | context.html = html;
165 | context.duration = msSince(start);
166 | }).catch((err) => {
167 | context.duration = msSince(start);
168 | return Promise.reject(err);
169 | });
170 | }
171 |
172 | recordError(error, token) {
173 | if (token && this.jobContexts[token]) {
174 | const context = this.jobContexts[token];
175 | context.statusCode = context.statusCode === 200 ? 500 : context.statusCode;
176 | context.error = error;
177 | } else {
178 | this.error = error;
179 | this.statusCode = 500;
180 | }
181 | }
182 |
183 | getResult(token) {
184 | const context = this.jobContexts[token];
185 | return {
186 | name: context.name,
187 | html: context.html,
188 | meta: context.returnMeta,
189 | duration: context.duration,
190 | statusCode: context.statusCode,
191 | success: context.html !== null,
192 | error: context.error ? errorToSerializable(context.error) : null,
193 | };
194 | }
195 |
196 | getResults() {
197 | return {
198 | success: this.error === null,
199 | error: this.error,
200 | results: Object.keys(this.jobContexts).reduce((result, token) => {
201 | /* eslint no-param-reassign: 1 */
202 | result[token] = this.getResult(token);
203 | return result;
204 | }, {}),
205 | };
206 | }
207 | }
208 |
209 | export default BatchManager;
210 |
--------------------------------------------------------------------------------
/src/utils/lifecycle.js:
--------------------------------------------------------------------------------
1 | import logger from './logger';
2 |
3 | const MAX_LIFECYCLE_EXECUTION_TIME_IN_MS = 300;
4 | const PROMISE_TIMEOUT = {};
5 |
6 | /**
7 | * @typedef {Object} HypernovaPlugin
8 | */
9 |
10 | /**
11 | * Returns a predicate function to filter objects based on whether a method of the provided
12 | * name is present.
13 | * @param {String} name - the method name to find
14 | * @returns {Function} - the resulting predicate function
15 | */
16 | export function hasMethod(name) {
17 | return (obj) => typeof obj[name] === 'function';
18 | }
19 |
20 | /**
21 | * Creates a promise that resolves at the specified number of ms.
22 | *
23 | * @param ms
24 | * @returns {Promise}
25 | */
26 | export function raceTo(promise, ms, msg) {
27 | let timeout;
28 |
29 | return Promise.race([
30 | promise,
31 | new Promise((resolve) => {
32 | timeout = setTimeout(() => resolve(PROMISE_TIMEOUT), ms);
33 | }),
34 | ]).then((res) => {
35 | if (res === PROMISE_TIMEOUT) logger.info(msg, { timeout: ms });
36 | if (timeout) clearTimeout(timeout);
37 |
38 | return res;
39 | }).catch((err) => {
40 | if (timeout) clearTimeout(timeout);
41 |
42 | return Promise.reject(err);
43 | });
44 | }
45 |
46 | /**
47 | * Iterates through the plugins and calls the specified asynchronous lifecycle event,
48 | * returning a promise that resolves when they all are completed, or rejects if one of them
49 | * fails.
50 | *
51 | * The third `config` param gets passed into the lifecycle methods as the first argument. In
52 | * this case, the app lifecycle events expect the config instance to be passed in.
53 | *
54 | * This function is currently used for the lifecycle events `initialize` and `shutdown`.
55 | *
56 | * @param {String} lifecycle
57 | * @param {Array} plugins
58 | * @param {Config} config
59 | * @returns {Promise}
60 | * @param err {Error}
61 | */
62 | export function runAppLifecycle(lifecycle, plugins, config, error, ...args) {
63 | try {
64 | const promise = Promise.all(
65 | plugins.filter(hasMethod(lifecycle)).map((plugin) => plugin[lifecycle](config, error, ...args)),
66 | );
67 |
68 | return raceTo(
69 | promise,
70 | MAX_LIFECYCLE_EXECUTION_TIME_IN_MS,
71 | `App lifecycle method ${lifecycle} took too long.`,
72 | );
73 | } catch (err) {
74 | return Promise.reject(err);
75 | }
76 | }
77 |
78 | /**
79 | * Iterates through the plugins and calls the specified asynchronous lifecycle event,
80 | * returning a promise that resolves when they are all completed, or rejects if one of them
81 | * fails.
82 | *
83 | * This is meant to be used on lifecycle events both at the batch level and the job level. The
84 | * passed in BatchManager is used to get the corresponding context object for the plugin/job and
85 | * is passed in as the first argument to the plugin's method.
86 | *
87 | * This function is currently used for `batchStart/End` and `jobStart/End`.
88 | *
89 | * @param {String} lifecycle
90 | * @param {Array} plugins
91 | * @param {BatchManager} manager
92 | * @param {String} [token] - If provided, the job token to use to get the context
93 | * @returns {Promise}
94 | */
95 | export function runLifecycle(lifecycle, plugins, manager, token) {
96 | try {
97 | const promise = Promise.all(
98 | plugins
99 | .filter(hasMethod(lifecycle))
100 | .map((plugin) => plugin[lifecycle](manager.contextFor(plugin, token))),
101 | );
102 |
103 | return raceTo(
104 | promise,
105 | MAX_LIFECYCLE_EXECUTION_TIME_IN_MS,
106 | `Lifecycle method ${lifecycle} took too long.`,
107 | );
108 | } catch (err) {
109 | return Promise.reject(err);
110 | }
111 | }
112 |
113 | /**
114 | * Iterates through the plugins and calls the specified synchronous lifecycle event (when present).
115 | * Passes in the appropriate context object for the plugin/job.
116 | *
117 | * This function is currently being used for `afterRender` and `beforeRender`.
118 | *
119 | * @param {String} lifecycle
120 | * @param {Array} plugins
121 | * @param {BatchManager} manager
122 | * @param {String} [token]
123 | */
124 | export function runLifecycleSync(lifecycle, plugins, manager, token) {
125 | plugins
126 | .filter(hasMethod(lifecycle))
127 | .forEach((plugin) => plugin[lifecycle](manager.contextFor(plugin, token)));
128 | }
129 |
130 | /**
131 | * Iterates through the plugins and calls the specified synchronous `onError` handler
132 | * (when present).
133 | *
134 | * Passes in the appropriate context object, as well as the error.
135 | *
136 | * @param {Error} err
137 | * @param {Array} plugins
138 | * @param {BatchManager} manager
139 | * @param {String} [token]
140 | */
141 | export function errorSync(err, plugins, manager, token) {
142 | plugins
143 | .filter(hasMethod('onError'))
144 | .forEach((plugin) => plugin.onError(manager.contextFor(plugin, token), err));
145 | }
146 |
147 | /**
148 | * Runs through the job-level lifecycle events of the job based on the provided token. This includes
149 | * the actual rendering of the job.
150 | *
151 | * Returns a promise resolving when the job completes.
152 | *
153 | * @param {String} token
154 | * @param {Array} plugins
155 | * @param {BatchManager} manager
156 | * @returns {Promise}
157 | */
158 | export function processJob(token, plugins, manager) {
159 | return (
160 | // jobStart
161 | runLifecycle('jobStart', plugins, manager, token)
162 |
163 | .then(() => {
164 | // beforeRender
165 | runLifecycleSync('beforeRender', plugins, manager, token);
166 |
167 | // render
168 | return manager.render(token);
169 | })
170 | // jobEnd
171 | .then(() => {
172 | // afterRender
173 | runLifecycleSync('afterRender', plugins, manager, token);
174 |
175 | return runLifecycle('jobEnd', plugins, manager, token);
176 | })
177 | .catch((err) => {
178 | manager.recordError(err, token);
179 | errorSync(err, plugins, manager, token);
180 | })
181 | );
182 | }
183 |
184 | function processJobsSerially(jobs, plugins, manager) {
185 | return Object.keys(jobs).reduce(
186 | (chain, token) => chain.then(() => processJob(token, plugins, manager)),
187 | Promise.resolve(),
188 | );
189 | }
190 |
191 | function processJobsConcurrently(jobs, plugins, manager) {
192 | return Promise.all(
193 | Object.keys(jobs).map((token) => processJob(token, plugins, manager)),
194 | );
195 | }
196 |
197 | /**
198 | * Runs through the batch-level lifecycle events of a batch. This includes the processing of each
199 | * individual job.
200 | *
201 | * Returns a promise resolving when all jobs in the batch complete.
202 | *
203 | * @param jobs
204 | * @param {Array} plugins
205 | * @param {BatchManager} manager
206 | * @returns {Promise}
207 | */
208 | export function processBatch(jobs, plugins, manager, concurrent) {
209 | return (
210 | // batchStart
211 | runLifecycle('batchStart', plugins, manager)
212 |
213 | // for each job, processJob
214 | .then(() => {
215 | if (concurrent) {
216 | return processJobsConcurrently(jobs, plugins, manager);
217 | }
218 |
219 | return processJobsSerially(jobs, plugins, manager);
220 | })
221 |
222 | // batchEnd
223 | .then(() => runLifecycle('batchEnd', plugins, manager))
224 | .catch((err) => {
225 | manager.recordError(err);
226 | errorSync(err, plugins, manager);
227 | })
228 | );
229 | }
230 |
--------------------------------------------------------------------------------
/src/utils/logger.js:
--------------------------------------------------------------------------------
1 | import winston from 'winston';
2 |
3 | let logger = null;
4 |
5 | const OPTIONS = {
6 | level: 'info',
7 | colorize: true,
8 | timestamp: true,
9 | prettyPrint: process.env.NODE_ENV !== 'production',
10 | };
11 |
12 | const loggerInterface = {
13 | init(config, loggerInstance) {
14 | if (loggerInstance) {
15 | logger = loggerInstance;
16 | } else {
17 | const options = { ...OPTIONS, ...config };
18 |
19 | logger = new winston.Logger({
20 | transports: [
21 | new winston.transports.Console(options),
22 | ],
23 | });
24 | }
25 |
26 | delete loggerInterface.init;
27 | },
28 |
29 | error(message, meta) {
30 | return logger.log('error', message, meta);
31 | },
32 |
33 | info(message, meta) {
34 | return logger.log('info', message, meta);
35 | },
36 | };
37 |
38 | export default loggerInterface;
39 |
--------------------------------------------------------------------------------
/src/utils/renderBatch.js:
--------------------------------------------------------------------------------
1 | import BatchManager from './BatchManager';
2 | import { processBatch } from './lifecycle';
3 | import logger from './logger';
4 |
5 | export default (config, isClosing) => (req, res) => {
6 | // istanbul ignore if
7 | if (isClosing()) {
8 | logger.info('Starting request when closing!');
9 | }
10 | const jobs = req.body;
11 |
12 | const manager = new BatchManager(req, res, jobs, config);
13 |
14 | return processBatch(jobs, config.plugins, manager, config.processJobsConcurrently)
15 | .then(() => {
16 | // istanbul ignore if
17 | if (isClosing()) {
18 | logger.info('Ending request when closing!');
19 | }
20 | return res.status(manager.statusCode).json(manager.getResults()).end();
21 | })
22 | .catch(() => res.status(manager.statusCode).end());
23 | };
24 |
--------------------------------------------------------------------------------
/src/worker.js:
--------------------------------------------------------------------------------
1 | import bodyParser from 'body-parser';
2 |
3 | import './environment';
4 | import logger from './utils/logger';
5 | import renderBatch from './utils/renderBatch';
6 | import { runAppLifecycle, errorSync, raceTo } from './utils/lifecycle';
7 | import BatchManager from './utils/BatchManager';
8 |
9 | const attachMiddleware = (app, config) => {
10 | app.use(bodyParser.json(config.bodyParser));
11 | };
12 |
13 | const attachEndpoint = (app, config, callback) => {
14 | app.post(config.endpoint, renderBatch(config, callback));
15 | };
16 |
17 | function exit(code) {
18 | return () => process.exit(code);
19 | }
20 |
21 | class Server {
22 | constructor(app, config, callback) {
23 | this.server = null;
24 | this.app = app;
25 | this.config = config;
26 | this.callback = callback;
27 |
28 | this.closing = false;
29 |
30 | this.close = this.close.bind(this);
31 | this.errorHandler = this.errorHandler.bind(this);
32 | this.shutDownSequence = this.shutDownSequence.bind(this);
33 | }
34 |
35 | close() {
36 | return new Promise((resolve) => {
37 | if (!this.server) {
38 | resolve();
39 | return;
40 | }
41 |
42 | try {
43 | this.closing = true;
44 | this.server.close((e) => {
45 | if (e) { logger.info('Ran into error during close', { stack: e.stack }); }
46 | resolve();
47 | });
48 | } catch (e) {
49 | logger.info('Ran into error on close', { stack: e.stack });
50 | resolve();
51 | }
52 | });
53 | }
54 |
55 | shutDownSequence(error, req, code = 1) {
56 | if (error) {
57 | logger.info(error.stack);
58 | }
59 |
60 | raceTo(this.close(), 1000, 'Closing the worker took too long.')
61 | .then(() => runAppLifecycle('shutDown', this.config.plugins, this.config, error, req))
62 | .then(exit(code))
63 | .catch(exit(code));
64 | }
65 |
66 | errorHandler(err, req, res, next) { // eslint-disable-line no-unused-vars
67 | // If there is an error with body-parser and the status is set then we can safely swallow
68 | // the error and report it.
69 | // Here are a list of errors https://github.com/expressjs/body-parser#errors
70 | if (err.status && err.status >= 400 && err.status < 600) {
71 | logger.info('Non-fatal error encountered.');
72 | logger.info(err.stack);
73 |
74 | res.status(err.status).end();
75 |
76 | // In a promise in case one of the plugins throws an error.
77 | new Promise(() => { // eslint-disable-line no-new
78 | const manager = new BatchManager(req, res, req.body, this.config);
79 | errorSync(err, this.config.plugins, manager);
80 | });
81 |
82 | return;
83 | }
84 | this.shutDownSequence(err, req, 1);
85 | }
86 |
87 | initialize() {
88 | // run through the initialize methods of any plugins that define them
89 | runAppLifecycle('initialize', this.config.plugins, this.config)
90 | .then(() => {
91 | this.server = this.app.listen(...this.config.listenArgs, this.callback);
92 | return null;
93 | })
94 | .catch(this.shutDownSequence);
95 | }
96 | }
97 |
98 | const initServer = (app, config, callback) => {
99 | const server = new Server(app, config, callback);
100 |
101 | // Middleware
102 | app.use(server.errorHandler);
103 |
104 | // Last safety net
105 | process.on('uncaughtException', server.errorHandler);
106 |
107 | // if all the workers are ready then we should be good to start accepting requests
108 | process.on('message', (msg) => {
109 | if (msg === 'kill') {
110 | server.shutDownSequence(null, null, 0);
111 | }
112 | });
113 |
114 | server.initialize();
115 |
116 | return server;
117 | };
118 |
119 | const worker = (app, config, onServer, workerId) => {
120 | // ===== Middleware =========================================================
121 | attachMiddleware(app, config);
122 |
123 | if (onServer) {
124 | onServer(app, process);
125 | }
126 |
127 | let server;
128 |
129 | // ===== Routes =============================================================
130 | // server.closing
131 | attachEndpoint(app, config, () => server && server.closing);
132 |
133 | function registerSignalHandler(sig) {
134 | process.on(sig, () => {
135 | logger.info(`Hypernova worker got ${sig}. Going down`);
136 | server.shutDownSequence(null, null, 0);
137 | });
138 | }
139 |
140 | // Gracefully shutdown the worker when not running in a cluster (devMode = true)
141 | if (config.devMode) {
142 | ['SIGTERM', 'SIGINT'].map(registerSignalHandler);
143 | }
144 |
145 | // ===== initialize server's nuts and bolts =================================
146 | server = initServer(app, config, () => {
147 | if (process.send) {
148 | // tell our coordinator that we're ready to start receiving requests
149 | process.send({ workerId, ready: true });
150 | }
151 |
152 | logger.info('Connected', { listen: config.listenArgs });
153 | });
154 | };
155 |
156 | worker.attachMiddleware = attachMiddleware;
157 | worker.attachEndpoint = attachEndpoint;
158 | worker.initServer = initServer;
159 | worker.Server = Server;
160 |
161 | export default worker;
162 |
--------------------------------------------------------------------------------
/test/BatchManager-test.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import sinon from 'sinon-sandbox';
3 |
4 | import { makeJob, COMPONENT_NAME } from './helper';
5 | import BatchManager from '../lib/utils/BatchManager';
6 |
7 | function mockPlugin() {
8 | return {
9 | initialize: sinon.stub(),
10 | batchStart: sinon.stub(),
11 | jobStart: sinon.stub(),
12 | beforeRender: sinon.stub(),
13 | afterRender: sinon.stub(),
14 | onError: sinon.stub(),
15 | jobEnd: sinon.stub(),
16 | batchEnd: sinon.stub(),
17 | shutDown: sinon.stub(),
18 | };
19 | }
20 |
21 | const jobs = {
22 | foo: makeJob(),
23 | bar: makeJob(),
24 | baz: {
25 | name: 'baz',
26 | data: {},
27 | },
28 | };
29 |
30 | jobs.bar.name = 'bar'; // component not registered
31 |
32 | const req = {};
33 | const res = {};
34 | const _strategies = {
35 | [COMPONENT_NAME]: sinon.stub().returns('html'),
36 | baz: sinon.stub().returns(undefined),
37 | };
38 | const config = {
39 | getComponent(name) {
40 | return _strategies[name];
41 | },
42 | plugins: {},
43 | };
44 |
45 | describe('BatchManager', () => {
46 | let plugins;
47 | let manager;
48 |
49 | beforeEach(() => {
50 | plugins = [
51 | mockPlugin(),
52 | mockPlugin(),
53 | ];
54 | config.plugins = plugins;
55 | manager = new BatchManager(req, res, jobs, config);
56 | });
57 |
58 | context('request contexts', () => {
59 | it('returns a plugin data map that persists across the plugin', () => {
60 | const context1 = manager.contextFor(plugins[0], 'foo');
61 | const context2 = manager.contextFor(plugins[0], 'foo');
62 | const context3 = manager.contextFor(plugins[1], 'foo');
63 |
64 | context1.data.set('foo', 'bar');
65 | assert.equal(context2.data.get('foo'), 'bar');
66 | assert.isUndefined(context3.data.get('foo'));
67 | });
68 |
69 | it('contains information about the specific job', () => {
70 | const context1 = manager.contextFor(plugins[0], 'foo');
71 | assert.equal(context1.token, 'foo');
72 | const context2 = manager.contextFor(plugins[0], 'bar');
73 | assert.equal(context2.token, 'bar');
74 | });
75 | });
76 |
77 | context('request contexts', () => {
78 | it('contains information about the batch', () => {
79 | const context1 = manager.contextFor(plugins[0]);
80 | assert.deepEqual(context1.tokens, Object.keys(jobs));
81 | });
82 | });
83 |
84 | describe('.render()', () => {
85 | it('sets the html and duration for the right context', (done) => {
86 | manager.render('foo').then(() => {
87 | const context = manager.jobContexts.foo;
88 | assert.equal(context.html, 'html');
89 | assert.equal(context.statusCode, 200);
90 | assert.isNotNull(context.duration);
91 |
92 | done();
93 | });
94 | });
95 |
96 | it('fails if component is not registered', (done) => {
97 | manager.render('bar').catch((err) => {
98 | assert.equal(err.message, 'Component "bar" not registered');
99 | done();
100 | });
101 | });
102 |
103 | it('fails when a component returns falsy html', (done) => {
104 | manager.render('baz').catch((err) => {
105 | assert.equal(
106 | err.message,
107 | 'HTML was not returned to Hypernova, this is most likely an error within your application. Check your logs for any uncaught errors and/or rejections.',
108 | );
109 | done();
110 | });
111 | });
112 | });
113 |
114 | describe('.recordError()', () => {
115 | it('sets error and status code for the jobContext, when token is present', () => {
116 | manager.recordError(new Error(), 'foo');
117 | const context = manager.contextFor(plugins[0], 'foo');
118 | assert.equal(context.statusCode, 500);
119 | });
120 |
121 | it('sets error and status code for the batch, when no token is present', () => {
122 | manager.recordError(new Error());
123 | assert.equal(manager.statusCode, 500);
124 | });
125 | });
126 |
127 | describe('.getResult()', () => {
128 | it('returns an object with the html of the right jobContext', (done) => {
129 | manager.render('foo').then(() => {
130 | const result = manager.getResult('foo');
131 | assert.equal(result.html, 'html');
132 | assert.isTrue(result.success);
133 | assert.isNull(result.error);
134 | done();
135 | });
136 | });
137 | it('returns an object with the html of the right jobContext', () => {
138 | manager.recordError(new Error(), 'bar');
139 | const result = manager.getResult('bar');
140 | assert.isFalse(result.success);
141 | assert.isNotNull(result.error);
142 | });
143 | });
144 |
145 | describe('.getResults()', () => {
146 | it('returns an object with keys of tokens of each job', (done) => {
147 | manager.render('foo').then(() => {
148 | manager.recordError(new Error(), 'bar');
149 | const response = manager.getResults();
150 | assert.isDefined(response.success);
151 | assert.isDefined(response.error);
152 | assert.isDefined(response.results);
153 | assert.isDefined(response.results.foo);
154 | assert.isDefined(response.results.bar);
155 | assert.equal(response.results.foo.html, 'html');
156 |
157 | done();
158 | });
159 | });
160 |
161 | it('contains a duration even if there is an error', (done) => {
162 | manager.render('bar').catch(() => {
163 | const response = manager.getResults();
164 | assert.isDefined(response.results.bar.duration);
165 | assert.isNumber(response.results.bar.duration);
166 |
167 | done();
168 | });
169 | });
170 | });
171 | });
172 |
--------------------------------------------------------------------------------
/test/Module-test.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import has from 'has';
3 | import { Module } from '../server';
4 | import mutableArray from './mutableArray';
5 |
6 | function run(code) {
7 | const name = __filename;
8 |
9 | const module = new Module(name);
10 | module.load(name);
11 | module._compile(code, name);
12 |
13 | return module.exports;
14 | }
15 |
16 | describe('Module', () => {
17 | it('does not leak globals across requests', () => {
18 | global.foo = 10;
19 | const code = `
20 | global.foo = global.foo || 0;
21 | global.foo += 1;
22 | `;
23 | run(code);
24 | assert(global.foo === 10, 'our environment\'s global was unaffected');
25 | run(code);
26 | assert(global.foo === 10, 'our environment\'s global was unaffected after a second run');
27 | });
28 |
29 | it('loads a module and return the instance', () => {
30 | const module = Module.load('./test/mutableArray.js');
31 | assert(has(module, 'exports') === true, 'module has exports property');
32 | assert.isArray(module.exports, 'module.exports is our array');
33 | });
34 |
35 | it('should not be able to mutate singletons', () => {
36 | assert(mutableArray.length === 0, 'our array is empty');
37 |
38 | mutableArray.push(1, 2, 3);
39 |
40 | assert(mutableArray.length === 3, 'our array has a length of 3');
41 |
42 | const code = `
43 | var mutableArray = require('./mutableArray');
44 | mutableArray.push(1);
45 | module.exports = mutableArray;
46 | `;
47 |
48 | const arr = run(code);
49 |
50 | assert(mutableArray !== arr, 'both arrays do not equal each other');
51 | assert(arr.length === 1, 'returned mutableArray has length of 1');
52 | assert(mutableArray.length === 3, 'our array still has a length of 3');
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/test/a.js:
--------------------------------------------------------------------------------
1 | global.a = 1;
2 |
--------------------------------------------------------------------------------
/test/b.js:
--------------------------------------------------------------------------------
1 | global.b = 2;
2 | export default 2;
3 |
--------------------------------------------------------------------------------
/test/checkIso.js:
--------------------------------------------------------------------------------
1 | import cheerio from 'cheerio';
2 | import { assert } from 'chai';
3 |
4 | export default {
5 | isIsomorphic(html) {
6 | const $ = cheerio.load(html);
7 | assert.ok(/___iso-html___/.test($('div').first().attr('class')), 'iso html exists');
8 | assert.ok(/___iso-state___/.test($('div').last().attr('class')), 'iso state exists');
9 | },
10 |
11 | isNotIsomorphic(html) {
12 | const $ = cheerio.load(html);
13 | assert.notOk(/___iso-html___/.test($('div').first().attr('class')), 'iso html does not exist');
14 | assert.notOk(/___iso-state___/.test($('div').last().attr('class')), 'iso state does not exist');
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/test/client-test.js:
--------------------------------------------------------------------------------
1 | import cheerio from 'cheerio';
2 | import sinon from 'sinon-sandbox';
3 | import wrap from 'mocha-wrap';
4 | import { assert } from 'chai';
5 | import { serialize, load } from '..';
6 |
7 | function cheerioToDOM($, className) {
8 | return $(className).map((i, cheerioObj) => {
9 | const node = cheerioObj;
10 | node.nodeName = node.name.toUpperCase();
11 | node.innerHTML = $(node).html();
12 | node.getAttribute = (attr) => $(node).data(attr.replace('data-', ''));
13 | return node;
14 | })[0];
15 | }
16 |
17 | wrap().withGlobal('document', () => ({}))
18 | .withGlobal('window', () => ({}))
19 | .describe('hypernova client', () => {
20 | let result;
21 | beforeEach(() => {
22 | result = serialize('Component3', 'Hello World!
', { name: 'Serenity' });
23 | });
24 |
25 | it('should load up the DOM', () => {
26 | const $ = cheerio.load(result);
27 |
28 | const spy = sinon.spy();
29 |
30 | global.document.querySelector = (className) => {
31 | spy(className);
32 | return cheerioToDOM($, className);
33 | };
34 | global.document.querySelectorAll = (classname) => [cheerioToDOM($, classname)];
35 |
36 | // Calling it again for the client.
37 | load('Component3');
38 |
39 | assert.ok(spy.calledOnce, 'our spy was called');
40 | });
41 |
42 | it('should not be called unless there is a node', () => {
43 | global.document = {
44 | querySelector() {
45 | return null;
46 | },
47 | querySelectorAll() {
48 | return [];
49 | },
50 | };
51 |
52 | const arr = load('foo');
53 |
54 | assert.ok(arr.length === 0);
55 |
56 | delete global.document;
57 | });
58 |
59 | it('should be called if there is a node', () => {
60 | const $ = cheerio.load(result);
61 |
62 | global.document = {
63 | querySelector(className) {
64 | return cheerioToDOM($, className);
65 | },
66 | querySelectorAll(className) {
67 | return [cheerioToDOM($, className)];
68 | },
69 | };
70 |
71 | load('Component3').forEach(({ node, data }) => {
72 | assert.isDefined(node);
73 | assert.isObject(data, 'state is an object');
74 | assert.equal(data.name, 'Serenity', 'state obj has proper state');
75 | });
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/test/components/HypernovaExample.js:
--------------------------------------------------------------------------------
1 | const hypernova = require('../..');
2 |
3 | module.exports = hypernova({
4 | server: function () {},
5 | client: function () {},
6 | });
7 |
--------------------------------------------------------------------------------
/test/components/nested/component.bundle.js:
--------------------------------------------------------------------------------
1 | const hypernova = require('../../..');
2 |
3 | module.exports = hypernova({
4 | server() {},
5 | client() {},
6 | });
7 |
--------------------------------------------------------------------------------
/test/coordinator-test.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import os from 'os';
3 | import sinon from 'sinon';
4 | import { getDefaultCPUs, getWorkerCount } from '../lib/coordinator';
5 |
6 | describe('coordinator', () => {
7 | it('default method returns correct number of cpus', () => {
8 | assert.equal(getDefaultCPUs(5), 4, 'getDefaultCPUs returns n - 1 CPUs');
9 |
10 | assert.throws(getDefaultCPUs, TypeError, 'getDefaultCPUs must accept a positive integer');
11 |
12 | assert.throws(() => {
13 | getDefaultCPUs('three');
14 | }, TypeError, 'getDefaultCPUs must accept a positive integer');
15 |
16 | assert.throws(() => {
17 | getDefaultCPUs(0);
18 | }, TypeError, 'getDefaultCPUs must accept a positive integer');
19 | });
20 |
21 | it('uses the correct number of cpus', () => {
22 | const sandbox = sinon.sandbox.create();
23 | const dummyCPUs = Array.from({ length: 5 }, () => ({}));
24 | sandbox.stub(os, 'cpus').returns(dummyCPUs);
25 |
26 | assert.equal(getWorkerCount(), dummyCPUs.length - 1, 'getWorkerCount defaults to all available cpus minus one');
27 | assert.equal(getWorkerCount(() => 3), 3, 'getWorkerCount uses specified cpus');
28 |
29 | assert.throws(() => {
30 | getWorkerCount(3);
31 | }, TypeError, 'getCPUs must be a function');
32 |
33 | assert.throws(() => {
34 | getWorkerCount(() => 'three');
35 | }, TypeError, 'getCPUs must return a positive integer');
36 |
37 | assert.throws(() => {
38 | getWorkerCount(() => 0);
39 | }, TypeError, 'getCPUs must return a positive integer');
40 |
41 | sandbox.restore();
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/test/createGetComponent-test.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import path from 'path';
3 | import { createGetComponent } from '../server';
4 |
5 | describe('createGetComponent', () => {
6 | const files = {
7 | HypernovaExample: path.resolve(path.join('test', 'components', 'HypernovaExample.js')),
8 | };
9 |
10 | const getComponent = createGetComponent(files);
11 |
12 | it('returns the module if it exists', () => {
13 | const component = getComponent('HypernovaExample');
14 | assert(component !== null, 'HypernovaExample exists');
15 | });
16 |
17 | it('returns null if it does not exist', () => {
18 | const component = getComponent('FooBarBazz');
19 | assert.isNull(component, 'component does not exist');
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/test/createVM-test.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import { createVM } from '../server';
3 |
4 | describe('createVM', () => {
5 | let vm;
6 |
7 | beforeEach(() => {
8 | vm = createVM();
9 | });
10 |
11 | it('runs the code', () => {
12 | const code = `
13 | module.exports = 12;
14 | `;
15 | const num = vm.run('test.js', code);
16 |
17 | assert(num === 12, 'returned value was given');
18 | });
19 |
20 | it('caches module.exports', () => {
21 | process.foo = 0;
22 | const code = `
23 | process.foo += 1;
24 | module.exports = process.foo;
25 | `;
26 |
27 | const num = vm.run('test.js', code);
28 |
29 | assert(num === 1, 'the resulting code was incremented');
30 |
31 | const nextNum = vm.run('test.js', code);
32 |
33 | assert(nextNum === 1, 'the module.exports was cached');
34 | });
35 |
36 | it('flushes the cache', () => {
37 | vm.run('test.js', '');
38 | assert(vm.exportsCache.itemCount === 1, 'the cache has 1 entry');
39 | vm.exportsCache.reset();
40 | assert(vm.exportsCache.itemCount === 0, 'the cache was reset');
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/test/escape-test.js:
--------------------------------------------------------------------------------
1 | import cheerio from 'cheerio';
2 | import wrap from 'mocha-wrap';
3 | import { assert } from 'chai';
4 | import { serialize, toScript, fromScript } from '..';
5 |
6 | describe('escaping', () => {
7 | it('escapes', () => {
8 | const html = serialize('foo', '', { foo: '', bar: '>' });
9 |
10 | assert.include(html, ' ({}))
16 | .describe('with fromScript', () => {
17 | it('loads the escaped content correctly', () => {
18 | const html = toScript({ a: 'b' }, { foo: '', bar: '>', baz: '&' });
19 | const $ = cheerio.load(html);
20 |
21 | global.document.querySelector = () => ({ innerHTML: $($('script')[0]).html() });
22 |
23 | const res = fromScript({
24 | a: 'b',
25 | });
26 |
27 | assert.isObject(res);
28 |
29 | assert.equal(res.foo, '');
30 | assert.equal(res.bar, '>');
31 | assert.equal(res.baz, '&');
32 | });
33 |
34 | it('escapes multiple times the same, with interleaved decoding', () => {
35 | const makeHTML = () => toScript({ attr: 'key' }, {
36 | props: 'yay',
37 | needsEncoding: '" > ', // "needsEncoding" is necessary
38 | });
39 | const script1 = makeHTML();
40 | const script2 = makeHTML();
41 | assert.equal(script1, script2, 'two successive toScripts result in identical HTML');
42 |
43 | const $ = cheerio.load(script1);
44 |
45 | global.document.querySelector = () => ({ innerHTML: $($('script')[0]).html() });
46 |
47 | const res = fromScript({ attr: 'key' });
48 |
49 | const script3 = makeHTML();
50 | assert.equal(
51 | script1,
52 | script3,
53 | 'third toScript after a fromScript call results in the same HTML',
54 | );
55 |
56 | assert.isObject(res);
57 |
58 | assert.equal(res.props, 'yay');
59 | });
60 | });
61 |
62 | it('escapes quotes and fixes data attributes', () => {
63 | const markup = toScript({
64 | 'ZOMG-ok': 'yes',
65 | 'data-x': 'y',
66 | '1337!!!': 'w00t',
67 | '---valid': '',
68 | 'Is this ok?': '',
69 | 'weird-values': '"]',
70 | 'weird-values2': '"""',
71 | }, {});
72 |
73 | const $ = cheerio.load(markup);
74 | const $node = $('script');
75 |
76 | assert.isString($node.data('zomg-ok'));
77 | assert.isString($node.data('data-x'));
78 | assert.isString($node.data('1337'));
79 | assert.isString($node.data('---valid'));
80 | assert.isString($node.data('isthisok'));
81 |
82 | assert.equal($node.data('weird-values'), '"]');
83 | assert.equal($node.data('weird-values2'), '"""');
84 |
85 | assert.isUndefined($node.data('ZOMG-ok'));
86 | assert.isUndefined($node.data('x'));
87 | assert.isUndefined($node.data('Is this ok?'));
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/test/getFiles-test.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import path from 'path';
3 |
4 | import { getFiles } from '../server';
5 |
6 | describe('getFiles', () => {
7 | it('retrieves files', () => {
8 | const files = getFiles(path.join('test', 'components'));
9 | assert(files.length, 2);
10 | assert.property(files[0], 'name');
11 | assert.property(files[0], 'path');
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/test/helper.js:
--------------------------------------------------------------------------------
1 | export const COMPONENT_NAME = 'HypernovaExampleReact.js';
2 |
3 | export function makeJob(props) {
4 | return {
5 | name: COMPONENT_NAME,
6 | data: props,
7 | };
8 | }
9 |
--------------------------------------------------------------------------------
/test/hypernova-runner-test.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import sinon from 'sinon-sandbox';
3 | import wrap from 'mocha-wrap';
4 |
5 | import hypernova from '..';
6 |
7 | describe('the runner', () => {
8 | it('runs server if window is not defined', () => {
9 | const server = sinon.spy();
10 | hypernova({ server });
11 | assert.ok(server.calledOnce);
12 | });
13 |
14 | wrap().withGlobal('window', () => ({})).it('runs client when window exists', () => {
15 | const client = sinon.spy();
16 | hypernova({ client });
17 | assert.ok(client.calledOnce);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/test/index-test.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import { DATA_KEY, DATA_ID } from '../lib';
3 |
4 | describe('hypernova', () => {
5 | it('DATA_KEY constant should be importable', () => {
6 | assert.equal(DATA_KEY, 'hypernova-key');
7 | });
8 |
9 | it('DATA_ID constant should be importable', () => {
10 | assert.equal(DATA_ID, 'hypernova-id');
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/test/init.js:
--------------------------------------------------------------------------------
1 | import 'airbnb-js-shims';
2 |
3 | import sinon from 'sinon-sandbox';
4 |
5 | afterEach(() => {
6 | sinon.restore();
7 | });
8 |
--------------------------------------------------------------------------------
/test/lifecycle-test.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import sinon from 'sinon';
3 |
4 | import '../lib/environment';
5 | import { makeJob } from './helper';
6 | import * as lifecycle from '../lib/utils/lifecycle';
7 | import BatchManager from '../lib/utils/BatchManager';
8 |
9 | function mockPlugin() {
10 | return {
11 | initialize: sinon.stub(),
12 | batchStart: sinon.stub(),
13 | jobStart: sinon.stub(),
14 | beforeRender: sinon.stub(),
15 | afterRender: sinon.stub(),
16 | onError: sinon.stub(),
17 | jobEnd: sinon.stub(),
18 | batchEnd: sinon.stub(),
19 | shutDown: sinon.stub(),
20 | };
21 | }
22 |
23 | function batchManagerInstance(jobs, plugins) {
24 | return new BatchManager({}, {}, jobs, { plugins });
25 | }
26 |
27 | describe('lifecycle', () => {
28 | const jobs = {
29 | foo: makeJob({ name: 'foo' }),
30 | bar: makeJob({ name: 'bar' }),
31 | };
32 |
33 | describe('.runAppLifecycle', () => {
34 | it('runs with sync methods', () => {
35 | const plugin = mockPlugin();
36 | const config = {};
37 |
38 | return lifecycle.runAppLifecycle('initialize', [plugin], config)
39 | .then(() => {
40 | assert.propertyVal(plugin.initialize, 'callCount', 1);
41 | assert.deepEqual(plugin.initialize.args[0][0], config);
42 | });
43 | });
44 |
45 | it('runs with async methods', () => {
46 | const config = {};
47 | const plugin = mockPlugin();
48 | let resolved = false;
49 |
50 | const promise = new Promise((resolve) => {
51 | setTimeout(() => {
52 | resolved = true;
53 | resolve();
54 | }, 20);
55 | });
56 |
57 | plugin.initialize = sinon.stub().returns(promise);
58 |
59 | return lifecycle.runAppLifecycle('initialize', [plugin], config)
60 | .then(() => {
61 | assert.propertyVal(plugin.initialize, 'callCount', 1);
62 | assert.deepEqual(plugin.initialize.args[0][0], config);
63 | assert.isTrue(resolved);
64 | });
65 | });
66 |
67 | it('runs with multiple plugins', () => {
68 | const config = {};
69 | const plugins = [mockPlugin(), mockPlugin(), mockPlugin()];
70 | plugins[0].initialize = sinon.stub().returns(Promise.resolve());
71 | plugins[1].initialize = sinon.stub().returns(Promise.resolve());
72 |
73 | return lifecycle.runAppLifecycle('initialize', plugins, config)
74 | .then(() => {
75 | assert.equal(plugins[0].initialize.callCount, 1);
76 | assert.deepEqual(plugins[0].initialize.args[0][0], config);
77 | assert.equal(plugins[1].initialize.callCount, 1);
78 | assert.deepEqual(plugins[1].initialize.args[0][0], config);
79 | assert.equal(plugins[2].initialize.callCount, 1);
80 | assert.deepEqual(plugins[2].initialize.args[0][0], config);
81 | });
82 | });
83 | });
84 |
85 | describe('.runLifecycle', () => {
86 | it('runs with sync methods', () => {
87 | const plugin = mockPlugin();
88 | const manager = batchManagerInstance(jobs, [plugin]);
89 |
90 | return lifecycle.runLifecycle('jobStart', [plugin], manager, 'foo')
91 | .then(() => {
92 | assert.equal(plugin.jobStart.callCount, 1, 'calls the method passed in');
93 | assert.deepEqual(plugin.jobStart.args[0][0], manager.contextFor(plugin, 'foo'));
94 | });
95 | });
96 |
97 | it('runs with async methods', () => {
98 | const plugins = [mockPlugin(), mockPlugin()];
99 | const manager = batchManagerInstance(jobs, plugins);
100 |
101 | let resolved = false;
102 | const promise = new Promise((resolve) => {
103 | setTimeout(() => {
104 | resolved = true;
105 | resolve();
106 | }, 20);
107 | });
108 | plugins[0].jobStart = sinon.stub().returns(promise);
109 |
110 | return lifecycle.runLifecycle('jobStart', plugins, manager, 'foo')
111 | .then(() => {
112 | const context = manager.contextFor(plugins[0], 'foo');
113 | assert.equal(plugins[0].jobStart.callCount, 1);
114 | assert.deepEqual(plugins[0].jobStart.args[0][0], context);
115 | assert.equal(plugins[1].jobStart.callCount, 1);
116 | assert.deepEqual(plugins[1].jobStart.args[0][0], context);
117 | assert.isTrue(resolved);
118 | });
119 | });
120 |
121 | it('runs with promises and sync methods', () => {
122 | const plugins = [mockPlugin(), mockPlugin(), mockPlugin()];
123 | plugins[0].jobStart = sinon.stub().returns(Promise.resolve());
124 | plugins[1].jobStart = sinon.stub().returns(Promise.resolve());
125 | const manager = batchManagerInstance(jobs, plugins);
126 |
127 | lifecycle.runLifecycle('jobStart', plugins, manager, 'foo')
128 | .then(() => {
129 | const context = manager.contextFor(plugins[0], 'foo');
130 | assert.equal(plugins[0].jobStart.callCount, 1);
131 | assert.deepEqual(plugins[0].jobStart.args[0][0], context);
132 | assert.equal(plugins[1].jobStart.callCount, 1);
133 | assert.deepEqual(plugins[1].jobStart.args[0][0], context);
134 | assert.equal(plugins[2].jobStart.callCount, 1);
135 | assert.deepEqual(plugins[2].jobStart.args[0][0], context);
136 | });
137 | });
138 | });
139 |
140 | describe('.runLifecycleSync', () => {
141 | it('runs methods synchronously', () => {
142 | const plugins = [mockPlugin(), mockPlugin(), mockPlugin()];
143 | const manager = batchManagerInstance(jobs, plugins);
144 | lifecycle.runLifecycleSync('beforeRender', plugins, manager, 'foo');
145 | const context = manager.contextFor(plugins[0], 'foo');
146 | assert.equal(plugins[0].beforeRender.callCount, 1);
147 | assert.deepEqual(plugins[0].beforeRender.args[0][0], context);
148 |
149 | assert.equal(plugins[1].beforeRender.callCount, 1);
150 | assert.deepEqual(plugins[1].beforeRender.args[0][0], context);
151 |
152 | assert.equal(plugins[2].beforeRender.callCount, 1);
153 | assert.deepEqual(plugins[2].beforeRender.args[0][0], context);
154 | });
155 | });
156 |
157 | describe('.errorSync', () => {
158 | it('calls onError synchronously with error object', () => {
159 | const err = new Error('message');
160 | const plugins = [mockPlugin(), mockPlugin(), mockPlugin()];
161 | const manager = batchManagerInstance(jobs, plugins);
162 | lifecycle.errorSync(err, plugins, manager, 'foo');
163 |
164 | const context = manager.contextFor(plugins[0], 'foo');
165 | assert.equal(plugins[0].onError.callCount, 1);
166 | assert.deepEqual(plugins[0].onError.args[0][0], context);
167 | assert.equal(plugins[0].onError.args[0][1], err);
168 |
169 | assert.equal(plugins[1].onError.callCount, 1);
170 | assert.deepEqual(plugins[1].onError.args[0][0], context);
171 | assert.equal(plugins[1].onError.args[0][1], err);
172 |
173 | assert.equal(plugins[1].onError.callCount, 1);
174 | assert.deepEqual(plugins[1].onError.args[0][0], context);
175 | assert.equal(plugins[1].onError.args[0][1], err);
176 | });
177 | });
178 |
179 | describe('.processJob', () => {
180 | let plugins;
181 | let manager;
182 |
183 | beforeEach(() => {
184 | plugins = [mockPlugin(), mockPlugin(), mockPlugin()];
185 | manager = batchManagerInstance(jobs, plugins);
186 |
187 | sinon.stub(manager, 'render');
188 | sinon.stub(manager, 'recordError');
189 | });
190 |
191 | it('calls lifecycle methods in correct order', () => (
192 | lifecycle.processJob('foo', plugins, manager).then(() => {
193 | sinon.assert.callOrder(
194 | plugins[0].jobStart,
195 | plugins[1].jobStart,
196 | plugins[2].jobStart,
197 |
198 | plugins[0].beforeRender,
199 | plugins[1].beforeRender,
200 | plugins[2].beforeRender,
201 |
202 | manager.render,
203 |
204 | plugins[0].afterRender,
205 | plugins[1].afterRender,
206 | plugins[2].afterRender,
207 |
208 | plugins[0].jobEnd,
209 | plugins[1].jobEnd,
210 | plugins[2].jobEnd,
211 | );
212 | })
213 | ));
214 |
215 | it('calls plugin methods with proper arguments', () => {
216 | const contexts = [
217 | manager.contextFor(plugins[0], 'foo'),
218 | manager.contextFor(plugins[1], 'foo'),
219 | manager.contextFor(plugins[2], 'foo'),
220 | ];
221 |
222 | return lifecycle.processJob('foo', plugins, manager).then(() => {
223 | sinon.assert.calledWith(plugins[0].jobStart, contexts[0]);
224 | sinon.assert.calledWith(plugins[1].jobStart, contexts[1]);
225 | sinon.assert.calledWith(plugins[2].jobStart, contexts[2]);
226 |
227 | sinon.assert.calledWith(plugins[0].beforeRender, contexts[0]);
228 | sinon.assert.calledWith(plugins[1].beforeRender, contexts[1]);
229 | sinon.assert.calledWith(plugins[2].beforeRender, contexts[2]);
230 |
231 | sinon.assert.calledWith(manager.render, 'foo');
232 |
233 | sinon.assert.calledWith(plugins[0].afterRender, contexts[0]);
234 | sinon.assert.calledWith(plugins[1].afterRender, contexts[1]);
235 | sinon.assert.calledWith(plugins[2].afterRender, contexts[2]);
236 |
237 | sinon.assert.calledWith(plugins[0].jobEnd, contexts[0]);
238 | sinon.assert.calledWith(plugins[1].jobEnd, contexts[1]);
239 | sinon.assert.calledWith(plugins[2].jobEnd, contexts[2]);
240 | });
241 | });
242 |
243 | it('on an error, fails fast', () => {
244 | plugins[0].beforeRender = sinon.stub().throws();
245 |
246 | return lifecycle.processJob('foo', plugins, manager)
247 | .then(() => {
248 | sinon.assert.called(plugins[0].jobStart);
249 | sinon.assert.called(plugins[1].jobStart);
250 | sinon.assert.called(plugins[2].jobStart);
251 |
252 | sinon.assert.notCalled(plugins[0].jobEnd);
253 | sinon.assert.notCalled(plugins[1].jobEnd);
254 | sinon.assert.notCalled(plugins[2].jobEnd);
255 | });
256 | });
257 |
258 | it('on an error, calls manager.recordError', () => {
259 | plugins[0].beforeRender = sinon.stub().throws();
260 |
261 | return lifecycle.processJob('foo', plugins, manager).then(() => {
262 | sinon.assert.called(manager.recordError);
263 | });
264 | });
265 |
266 | it('on an error, calls onError for plugins', () => {
267 | plugins[0].beforeRender = sinon.stub().throws();
268 |
269 | return lifecycle.processJob('foo', plugins, manager)
270 | .then(() => {
271 | sinon.assert.called(plugins[0].onError);
272 | sinon.assert.called(plugins[1].onError);
273 | sinon.assert.called(plugins[2].onError);
274 | });
275 | });
276 | });
277 |
278 | describe('.processBatch', () => {
279 | let plugins;
280 | let manager;
281 |
282 | beforeEach(() => {
283 | plugins = [mockPlugin(), mockPlugin(), mockPlugin()];
284 | manager = batchManagerInstance(jobs, plugins);
285 |
286 | sinon.stub(manager, 'render');
287 | sinon.stub(manager, 'recordError');
288 | });
289 |
290 | [true, false].forEach((concurrent) => {
291 | describe(`when concurrent is ${concurrent}`, () => {
292 | it('calls lifecycle methods in correct order', () => (
293 | lifecycle.processBatch(jobs, plugins, manager, concurrent)
294 | .then(() => {
295 | sinon.assert.callOrder(
296 | plugins[0].batchStart,
297 | plugins[1].batchStart,
298 | plugins[2].batchStart,
299 |
300 | // gets called once for each job
301 | manager.render,
302 | manager.render,
303 |
304 | plugins[0].batchEnd,
305 | plugins[1].batchEnd,
306 | plugins[2].batchEnd,
307 | );
308 | })
309 | ));
310 |
311 | it('on an error, fails fast', () => {
312 | plugins[0].batchStart = sinon.stub().throws();
313 |
314 | return lifecycle.processBatch(jobs, plugins, manager, concurrent)
315 | .then(() => {
316 | sinon.assert.called(plugins[0].batchStart);
317 | sinon.assert.notCalled(manager.render);
318 | sinon.assert.notCalled(plugins[0].batchEnd);
319 | });
320 | });
321 |
322 | it('on an error, calls manager.recordError', () => {
323 | plugins[0].batchStart = sinon.stub().throws();
324 |
325 | return lifecycle.processBatch(jobs, plugins, manager, concurrent)
326 | .then(() => {
327 | sinon.assert.called(manager.recordError);
328 | });
329 | });
330 |
331 | it('on an error, calls onError for plugins', () => {
332 | plugins[0].batchStart = sinon.stub().throws();
333 |
334 | return lifecycle.processBatch(jobs, plugins, manager, concurrent)
335 | .then(() => {
336 | sinon.assert.called(plugins[0].onError);
337 | sinon.assert.called(plugins[1].onError);
338 | sinon.assert.called(plugins[2].onError);
339 | });
340 | });
341 | });
342 | });
343 | });
344 | });
345 |
--------------------------------------------------------------------------------
/test/loadModules-test.js:
--------------------------------------------------------------------------------
1 | import { loadModules, createVM } from '../server';
2 |
3 | describe('loadModules', () => {
4 | it('loads the respective environment', () => {
5 | const environment = loadModules(require, [
6 | './a.js',
7 | './b.js',
8 | ]);
9 |
10 | const vm = createVM({
11 | environment,
12 | });
13 |
14 | vm.run('test/loadModules-test.js', `
15 | const assert = require('chai').assert;
16 |
17 | assert.isDefined(global.a);
18 | assert.isDefined(global.b);
19 | `);
20 | });
21 |
22 | it('works if one module is passed', () => {
23 | const environment = loadModules(require, [
24 | './a.js',
25 | ]);
26 |
27 | const vm = createVM({
28 | environment,
29 | });
30 |
31 | vm.run('test/loadModules-test.js', `
32 | const assert = require('chai').assert;
33 |
34 | assert.isDefined(global.a);
35 | `);
36 | });
37 |
38 | it('still works if a module that does not exist is passed', () => {
39 | const environment = loadModules(require, [
40 | './a.js',
41 | './does-not-exist.js',
42 | ]);
43 |
44 | const vm = createVM({
45 | environment,
46 | });
47 |
48 | vm.run('test/loadModules-test.js', `
49 | const assert = require('chai').assert;
50 |
51 | assert.isDefined(global.a);
52 | `);
53 | });
54 |
55 | it('still works if a module that does not exist is passed in first', () => {
56 | const environment = loadModules(require, [
57 | './does-not-exist.js',
58 | './a.js',
59 | ]);
60 |
61 | const vm = createVM({
62 | environment,
63 | });
64 |
65 | vm.run('test/loadModules-test.js', `
66 | const assert = require('chai').assert;
67 |
68 | assert.isDefined(global.a);
69 | `);
70 | });
71 | });
72 |
--------------------------------------------------------------------------------
/test/mutableArray.js:
--------------------------------------------------------------------------------
1 | module.exports = [];
2 |
--------------------------------------------------------------------------------
/test/renderBatch-test.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import renderBatch from '../lib/utils/renderBatch';
3 |
4 | /* eslint max-classes-per-file: 1 */
5 |
6 | class Response {
7 | status(status) {
8 | this._status = status;
9 | return this;
10 | }
11 |
12 | json(res) {
13 | this._json = res;
14 | return this;
15 | }
16 |
17 | end() {} // eslint-disable-line class-methods-use-this
18 |
19 | getResponse() {
20 | return {
21 | status: this._status,
22 | json: this._json,
23 | };
24 | }
25 | }
26 |
27 | class Request {
28 | constructor() {
29 | this.body = {
30 | a: {
31 | name: 'HypernovaExample',
32 | data: {},
33 | },
34 | };
35 | }
36 | }
37 |
38 | function makeExpress() {
39 | const req = new Request();
40 | const res = new Response();
41 |
42 | return { req, res };
43 | }
44 |
45 | describe('renderBatch', () => {
46 | [true, false].forEach((processJobsConcurrently) => {
47 | describe(`when processJobsConcurrently is ${processJobsConcurrently}`, () => {
48 | it('returns a batch properly', (done) => {
49 | const expressRoute = renderBatch({
50 | getComponent() {
51 | return null;
52 | },
53 | plugins: [],
54 | processJobsConcurrently,
55 | }, () => false);
56 |
57 | const { req, res } = makeExpress();
58 |
59 | expressRoute(req, res).then(() => {
60 | assert.isObject(res.getResponse());
61 |
62 | const { status, json } = res.getResponse();
63 |
64 | assert.isDefined(status);
65 |
66 | assert.equal(status, 200);
67 |
68 | assert.isTrue(json.success);
69 | assert.isNull(json.error);
70 |
71 | const { a } = json.results;
72 | assert.isDefined(a);
73 | assert.property(a, 'html');
74 | assert.property(a, 'meta');
75 | assert.property(a, 'duration');
76 | assert.property(a, 'success');
77 | assert.property(a, 'error');
78 |
79 | done();
80 | });
81 | });
82 |
83 | it('rejects a Promise with a string and its ok', (done) => {
84 | const expressRoute = renderBatch({
85 | getComponent() {
86 | return Promise.reject('Nope');
87 | },
88 | plugins: [],
89 | processJobsConcurrently,
90 | }, () => false);
91 |
92 | const { req, res } = makeExpress();
93 |
94 | expressRoute(req, res).then(() => {
95 | const { json } = res.getResponse();
96 | const { a } = json.results;
97 |
98 | assert.equal(a.error.name, 'Error');
99 | assert.equal(a.error.message, 'Nope');
100 |
101 | done();
102 | });
103 | });
104 |
105 | it('rejects a Promise with a ReferenceError', (done) => {
106 | const expressRoute = renderBatch({
107 | getComponent() {
108 | return Promise.reject(new ReferenceError());
109 | },
110 | plugins: [],
111 | processJobsConcurrently,
112 | }, () => false);
113 |
114 | const { req, res } = makeExpress();
115 |
116 | expressRoute(req, res).then(() => {
117 | const { json } = res.getResponse();
118 | const { a } = json.results;
119 |
120 | assert.equal(a.error.name, 'ReferenceError');
121 |
122 | done();
123 | });
124 | });
125 |
126 | it('rejects a Promise with an Array', (done) => {
127 | const expressRoute = renderBatch({
128 | getComponent() {
129 | return Promise.reject([1, 2, 3]);
130 | },
131 | plugins: [],
132 | processJobsConcurrently,
133 | }, () => false);
134 |
135 | const { req, res } = makeExpress();
136 |
137 | expressRoute(req, res).then(() => {
138 | const { json } = res.getResponse();
139 | const { a } = json.results;
140 |
141 | assert.equal(a.error.name, 'Error');
142 | assert.equal(a.error.message, '1,2,3');
143 |
144 | done();
145 | });
146 | });
147 | });
148 | });
149 | });
150 |
--------------------------------------------------------------------------------
/test/server-test.js:
--------------------------------------------------------------------------------
1 | import { assert } from 'chai';
2 | import express from 'express';
3 |
4 | describe('Hypernova server', () => {
5 | let hypernova;
6 | const getComponent = () => {};
7 |
8 | beforeEach(() => {
9 | try {
10 | [
11 | '../lib/utils/logger',
12 | '../lib/worker',
13 | '../lib/server',
14 | '../server',
15 | ].forEach((module) => delete require.cache[require.resolve(module)]);
16 |
17 | hypernova = require('../server'); // eslint-disable-line global-require
18 | } catch (e) {
19 | console.error('Couldnt remove dependecy or load the hypernova module.');
20 | }
21 | });
22 |
23 | it('blows up if hypernova does not get getComponent', () => {
24 | assert.throws(hypernova, TypeError);
25 | });
26 |
27 | it('blows up if hypernova gets `createApplication` that isnt a function', () => {
28 | assert.throws(
29 | () => hypernova({
30 | devMode: true,
31 | getComponent,
32 | createApplication: {} }),
33 | TypeError);
34 | });
35 |
36 | it('blows up if hypernova gets `createApplication` that doesnt return an express app', () => {
37 | assert.throws(
38 | () => hypernova({
39 | devMode: true,
40 | getComponent,
41 | createApplication: () => {} }),
42 | TypeError);
43 | });
44 |
45 | it('starts up the hypernova server without blowing up', () => {
46 | hypernova({ devMode: true, getComponent });
47 | });
48 |
49 | it('starts up the hypernova server and an express instance without blowing up', () => {
50 | const APP_TITLE = 'my custom express instance';
51 |
52 | const createApplication = () => {
53 | const app = express();
54 | app.locals.name = APP_TITLE;
55 |
56 | return app;
57 | };
58 |
59 | const hypernovaServer = hypernova({
60 | devMode: true,
61 | getComponent,
62 | createApplication,
63 | port: 8090,
64 | });
65 |
66 | assert.equal(APP_TITLE, hypernovaServer.locals.name);
67 | });
68 | });
69 |
--------------------------------------------------------------------------------