You can install Hyperstack either
- using a template - best for new applications (not working for Rails 6) yet;
- using a Rails generator - best for existing Rails apps;
- or manually walking through the detailed installation instructions below.
Even if you use the template or generator, later reading through the detailed installation instructions can be helpful to understand how the system fits together and the generators work.
- Rails 5.x Install Instructions
And for a full system as setup by the template or install generator you will need
- Yarn Install Instructions
This template will create a new Rails app with Webpacker from Hyperstack edge branch. This is the easiest way to get started.
Simply run the command below to create a new Rails app with Hyperstack all configured:
rails new MyApp -T -m https://rawgit.com/hyperstack-org/hyperstack/edge/install/rails-webpacker.rb
Note: The -T flag will not install minitest directories, leaving room for Rspec and Hyperspec. See the HyperSpec readme under "Tools" for more info.
foreman startto start Rails and the Hotloader- Navigate to
http://localhost:5000/
If you have an existing Rails app, you can use the built in generator to install Hyperstack. Best to create a new branch of course before trying this out.
- add
gem 'rails-hyperstack', "~> 1.0.alpha1.0"to your gem file - run
bundle install - run
bundle exec rails g hyperstack:install
Note: if you want to use the unreleased edge branch your gem specification will be:
gem 'rails-hyperstack', git: 'git://github.com/hyperstack-org/hyperstack.git', branch: 'edge', glob: 'ruby/*/*.gemspec'
bundle exec foreman startto start Rails and the Hotloader- Navigate to
http://localhost:5000/
Note that the generator will add a wild card route to the beginning of your routes file. This will let you immediately test Hyperstack, but will also mean that all of your existing routes are now unreachable. So after getting Hyperstack up, you will want to adjust things to your needs. See the first in the Manual Installation section for more info.
To manually install a complete Hyperstack system there are quite a few steps. However these can be broken down into six separate activities each of which will leave your system in a working and testable state:
- Adding the
rails-hyperstackgem - Adding and Mounting Hyperstack Components
- Installing the Hotloader
- Integrating with Webpacker
- Adding Policies and Integrating with ActiveRecord
Steps 3, 4 and 5 can be done in any order, and can be skipped as needed. The following sections review each of the steps.
Add
gem 'rails-hyperstack', '~> 1.0.alpha1.0'to your gem file.
Then bundle install. Your Rails app will continue to work as before.
Note: if you want to use the unreleased edge branch your rails-hyperstack gem specification will be:
gem 'rails-hyperstack', git: 'git://github.com/hyperstack-org/hyperstack.git', branch: 'edge', glob: 'ruby/*/*.gemspec'
Once you have added the rails-hyperstack gem to your system you can easily add a new component to your system by running the following command:
bundle exec rails g hyper:component TestApp --add-route="/test/(*others)"
After running this command you should be able to start your server
bundle exec rails s
and visit localhost:3000/test and you will see TestApp displayed.
This command accomplishes four tasks which you can also manually perform if you prefer:
- Insure that the
hyperstack-loaderis required in yourapplication.jsfile; - Insure that you have a
HyperComponentbase class defined; - Add a skeleton
TestAppcomponent and - Add a route to the
TestAppcomponent in your rails routes file.
Each time the hyper:install, hyper:component and hyper:router generators run they insure that the hyperstack-loader require is present, and will print a warning if it cannot successfully add it to the application.js file.
The hyperstack-loader is a dynamically generated asset manifest that will load all your client side Ruby code. It needs to be the last require in app/assets/javascripts/application.js file. That is it should be just before the final require_tree directive
// assets/javascripts/application.js
...
//= require hyperstack-loader
//= require_tree .Once this is added to your application.js file you will see the hyperstack asset manifest on the Rails console and the browser's debug console which looks like this:
require 'opal'
require 'browser' # CLIENT ONLY
require 'hyperstack-config'
require 'hyperstack/autoloader'
require 'hyperstack/autoloader_starter'
require 'hyper-state'
require 'react/react-source-browser' # CLIENT ONLY
require 'react_ujs'
require 'hyper-router'
require 'hyper-model'
require 'browser/delay' # CLIENT ONLY
require 'hyper-component'
require 'hyperstack/component/auto-import'
require 'hyper-operation'
require 'hyperstack/router/react-router-source'
require 'config/initializers/inflections.rb'
Note: By default you will get the entire Hyper Stack, but you can selectively remove parts you do not need in the hyperstack initializer file, which will be discussed later.
Each time the hyper:install, hyper:component and hyper:router generators run they insure that there is an application defined HyperComponent base class defined in the app/hyperstack/components/ directory. If not present one will be added.
All your Hyperstack code goes into the app/hyperstack directory, which is at the same level as app/models, app/views, app/controllers, etc.
Inside this directory are subdirectories for each of the different parts of you hyperstack application code. Components go in the app/hyperstack/components directory.
Like Rails models, and Rails controllers, Hyperstack components by convention inherit from an application defined base class. So while a typical Rails model inherits from the ApplicationRecord class, your Hyperstack components will inherit from the HyperComponent class.
The typical HyperComponent class definition looks like this:
# app/hyperstack/components/hyper_component.rb
class HyperComponent
# All component classes must include Hyperstack::Component
include Hyperstack::Component
# The Observable module adds state handling
include Hyperstack::State::Observable
# The following turns on the new style param accessor
# i.e. param :foo is accessed by the foo method
param_accessor_style :accessors
endWhen the generators run they check for this file, and if not present will add it.
Note having a HyperComponent base class is only convention. The Hyperstack system (except for the generators) assumes nothing about HyperComponent or how you define components. Any class that includes the
Hyperstack::Componentmodule will be a component. For example you may want to name your base classApplicationComponentto more closely following the rails convention.In this case you would have an
app/hyperstack/component/application_component.rbfile that defines yourApplicationComponentclass. Then your components would be subclasses ofApplicationComponent.You can even define several base classes. For example you might define a
StaticComponentclass which does not include theHyperstack::State:Observable, and thus create a static (functional) component class.To tell the generators to use a different base class name use the
--base-classswitch. For example--base-class=ApplicationComponent
All your components by convention are kept in the app/hyperstack/components directory. A simple component would look like this:
# app/hyperstack/components/test_app.rb
class TestApp < HyperComponent
render(DIV)
"TestApp" # render the string TestApp in a DIV
end
endOnce you add any .rb file to any subdirectory of app/hyperstack it will be added to the hyperstack-loader manifest for you.
The hyper:component and hyper:router generators add a skeleton component file like the one above, after insuring that the hyperstack-loader require and the HyperComponent base class are present in the correct places.
Now that you have a have a component it needs to be mounted for it to be rendered. You have three options for doing this depending on your needs.
The easiest way is to route to your top level component in your Rails routes file. For example if you have a component named TestApp you can route to it and mount it by adding the following line near the beginning of the config/routes.rb file:
get '/test/(*others)', to: 'hyperstack#test_app'Now all URLs beginning with test/ will bring up a page with TestApp mounted in it.
The first parameter to the
getroute method describes the path that will be matched. In this case/test/followed by any other sub path will match. When matched the built inhyperstackcontroller will accept the request, and will build a page with theTestAppcomponent mounted in it. Notice the translation oftest_apptoTestApp.
The hyper:component and hyper:router generators accept an optional add-route parameter which indicates that you want a route added. The default value for this parameter is the path /(*others):
... --add-route # adds path '/(*others)'
... --add-route='/fred' # adds path '/fred'
If you are adding Hyperstack to an existing application you may want to simply mount components somewhere in an existing view. To do this use the render_component view helper:
...
<% render_component 'TestApp' %>
...
will render the component named TestApp at that position in your view.
The
render_componenthelper can also pass parameters to the component so for example if you had a componentDisplayFormattedName, and your controller had set@nameyou might have this in your ERB file:... <% render_component 'DisplayFormattedName', name: @name %>
You can also use render_component in a controller in place of the standard Rails render method. Like the render_component view helper you can pass the component parameters in from the controller.
There are quite a few steps, but each has a specific, and understandable purpose.
The Hotloader watches your directories, and when client side files change, it will compile those files, and surgically update the client environment to contain the new code. The update process is near instantaneous, so it makes developing and debugging components easy.
There are three steps to installing the Hotloader:
- Importing Hotloader into your
hyperstack-loadermanifest; - Adding the
foremangem to yourGemfileand - Adding a
Procfileto the root of our application directory.
By default the Hotloader is not included in the hyperstack manifest so the first step is to add the config/initializers/hyperstack.rb initializer file, and import the Hotloader:
# config/initializers/hyperstack.rb
Hyperstack.configuration do |config|
config.import 'hyperstack/hotloader',
client_only: true if Rails.env.development?
endNote that we don't need or want the Hotloader running in test or production so we put the import on a switch, so the import only occurs in development.
Because you have changed the system manifest its best to clear the Rails cache to insure the new configuration is rebuilt, and not loaded from cache. In the shell run
rm -rf tmp/cache # clear the cacheThe Hotloader needs to run in a separate process, so bring up a separate terminal window and run
bundle exec hyperstack-hotloader -p 25222 -d app/hyperstackThis will tell the Hotloader to use port 25222, and to scan the app/hyperstack directory.
If for some reason you cannot use port 25222 you can change it, but you need to also configure this in the Hyperstack initializer file:
... config.hotloader_port = 12345 # override default of 25222 ...
Now that the Hotloader is running, you can start your Rails server the normal way, and refresh your browser page. You should now see require 'hyperstack/hotloader' # CLIENT ONLY added to the manifest, and you will also see a message indicating that your browser has connected to the Hotloader.
Now go into your editor and make a change to the component. You should see the browser window updating as soon as you save the file.
Having to start (and stop) two separate shells is painful so you can add the foreman gem which will manage all that for you. Add
gem `foreman` group: :developmentTo your gemfile and do a bundle install.
Now add the following Procfile to the root of your applications directory:
web: bundle exec rails s -b 0.0.0.0
hot-loader: bundle exec hyperstack-hotloader -p 25222 -d app/hyperstack
Now stop both our server and hotloader processes if they are still running, and then run
bundle exec foreman startWhich will have foreman start both the processes, and will also allow a single CTRL-C to cancel both processes.
By convention when using foreman we load the server over port 5000, so you will visit localhost:5000 instead of locahost:3000.
Using the Rails webpacker gem you can easily add other NPM (node package manager) assets to your Hyperstack application. This allows you Hyperstack components use any of the thousands of existing React component libraries, as well as packages like jQuery.
There is a bit of configuration needed to add webpacker to your application which can be done by running
bundle exec rails g hyperstack:webpackNote that you also need to have yarn installed Install Instructions
For details on how to import and use NPM packages in your application see Importing React Components
To manually install webpacker follow these steps:
Then run these commands to install Hyperstack's NPM dependencies using yarn:
yarn 'react', '16'
yarn 'react-dom', '16'
yarn 'react-router', '^5.0.0'
yarn 'react-router-dom', '^5.0.0'
yarn 'react_ujs', '^2.5.0'Because we are now managing these components via yarn, we don't want the Hyperstack loader to include them. So we use the Hyperstack cancel_import config directive in the config/initializers/hyperstack.rb file:
# config/initializers/hyperstack.rb
Hyperstack.configuration do |config|
config.cancel_import 'react/react-source-browser'
endOf course if you already have a hyperstack.rb config file, you will be just adding the one cancel_import line.
Create a new app/javascript/packs directory and add these two files:
//app/javascript/packs/client_and_server.js
// these packages will be loaded both during prerendering and on the client
React = require('react'); // react-js library
History = require('history'); // react-router history library
ReactRouter = require('react-router'); // react-router js library
ReactRouterDOM = require('react-router-dom'); // react-router DOM interface
ReactRailsUJS = require('react_ujs'); // interface to react-rails
// to add additional NPM packages call run yarn add package-name@version
// then add the require here.//app/javascript/packs/client_only.js
// add any requires for packages that will run client side only
ReactDOM = require('react-dom'); // react-js client side code
// jQuery = require('jquery');
// to add additional NPM packages call run yarn add package-name@version
// then add the require here.If configured Hyperstack will prerender pages serverside, so that the initial download to the client has your component tree already rendered into HTML.
Because prerendering runs on the server it does not have access to things like the current time, or the window object. NPM packages that can only sensibly be run on the client are included in the client_only.js pack file, while packages designed to be run in the server environment as well go into client_and_server.js
In config/initializers/assets.rb add the following line at the end of the file:
Rails.application.config.assets.paths <<
Rails.root.join('public', 'packs', 'js').to_sand in config/environments/test.rb add the following line before the final end statement:
# config/environments/test.rb
Rails.application.configure do
# other stuff...
config.assets.paths <<
Rails.root.join('public', 'packs-test', 'js').to_s
endAdd
gem 'webpacker'to your Gemfile then
bundle installbundle exec rails webpacker:installThis installs all the base webpacker stuff that Rails needs.
Hyperstack uses Policies to control client access to your Models and Operations (Hyperstacks implementation of ServiceObjects.)
Hyperstack shares the contents of the models, operations and shared directories with both the server and the client, so both server and client can access code in those directories.
Together these two mechanisms give you secure access to your models, operations, and other shared code on both the client and server.
- Create a Development Policy
- Move
app/models/application_record.rbtoapp/hyperstack/models/ - Create a stub
app/models/application_record.rbfile - Move any models you want to use on the client from the
app/modelsdirectory toapp/hyperstack/modelsdirectory
Hyperstack uses Policies to control communication between the client and server. Policies go in the app/policies directory. The following Policy will give unrestricted access to your models unless in production:
# app/policies/application_policy.rb
# Policies regulate access to your public models
# The following policy will open up full access (but only in development)
# The policy system is very flexible and powerful. See the documentation
# for complete details.
class Hyperstack::ApplicationPolicy
# Allow any session to connect:
always_allow_connection
# Send all attributes from all public models
regulate_all_broadcasts { |policy| policy.send_all }
# Allow all changes to models
allow_change(to: :all, on: [:create, :update, :destroy]) { true }
# allow remote access to all scopes - i.e. you can count or get a list of ids
# for any scope or relationship
ApplicationRecord.regulate_scope :all
end unless Rails.env.production?This policy specifies the following: 1. allow any client to connect to the Hyperstack::Application 2. the server will broadcast all data changes to the Hyperstack::Application 3. the application can create, update and destroy all models 4. the client can filter all models using any scope But only if we are not running in production!
As your application develops you can begin defining more restrictive policies (only allowing Users to see their own data for example)
Note: The policy mechanism does not depend on Pundit but is compatible with it.
Once you have a basic Policy defined the client can access your Rails models. For your ActiveRecord model class definitions to be visible on the client you need to move them to the app/hyperstack/models directory. This directory (along with app/hyperstack/operations and app/hyperstack/shared) are isomorphic directories. The code in these directories will be accessible on both the client and the server.
For example if you have the following class definition
class Todo < ApplicationRecord
scope :active, -> () { where(completed: false) }
scope :completed -> () { where(completed: true) }
endYou could then on the server execute Todo.active and get all the todos in the active scope. But as long as this class stays in the app/models directory it will not exist on the client.
Once you move the Todo class definition to app/hyperstack/models it will also be visible on the client, and Todo.active will now return a list of all active todos on the server and the client.
Because all your ActiveRecord models are (typically) defined as subclasses of ApplicationRecord, you need to also move the app/models/application_record.rb file to the app/hyperstack/models/ directory.
At this point everything will work, until you run a Rails migration. Then Rails will discover that there is no app/models/application_record.rb file (because you moved it) and will generate a new one for you!
So to prevent that you need to add this file to app/models/application_record.rb
# app/models/application_record.rb
# the presence of this file prevents rails migrations from recreating application_record.rb
# see https://github.com/rails/rails/issues/29407
# this will grab the real file from the hyperstack directory
require 'models/application_record.rb'Note that the last line is not a typo. Rails paths begin with the subdirectory name. So
'models/application_record.rb'means to search all directories for a file nameapplication_record.rbin themodelssubdirectory
Hyperstack will load code out of the app/hyperstack directory. Within this directory there are typically the following subdirectories:
app/hyperstack/components- Component classes (client only)app/hyperstack/models- Shared active record model classesapp/hyperstack/operations- Shared operation classesapp/hyperstack/stores- Other data stores (client only)app/hyperstack/shared- Misc shared modules and classesapp/hyperstack/lib- Client only libraries
These directories are all optional. The models, operations, and shared subdirectories are both loaded on the client, and will be also included as part of the Rails constant lookup search path.
Any other subdirectories will be treated as client only. The names listed above such as components, stores and lib are just conventions. For example you may prefer client_lib.
Note that you will still have the standard Rails
app/modelsdirectory, which can be used to keep server-only models. This is useful for models that will never be accessed from the client to reduce payload size. You can also add anapp/operationsdirectory if you wish to have Operations that only run on the server.This does not effect security. See the section 7 for how Policies are setup.
Rails models files normally inherit from the ApplicationModel class, so you must move the app/models/application_record.rb to app/hyperstack/models/application_record.rb so that it is accessible both on the client and server.
However Rails will automatically generate a new application_record.rb file if it does not find one in the app/models directory. To prevent this create a new app/models/application_record.rb that looks like this:
# app/models/application_record.rb
# the presence of this file prevents rails migrations from recreating application_record.rb
# see https://github.com/rails/rails/issues/29407
# this will grab the real file from the hyperstack directory
require 'models/application_record.rb'Note that the above is not a typo. Rails paths begin with the subdirectory name. So
'models/application_record.rb'means to search all directories for a file nameapplication_record.rbin themodelssubdirectory
Your server side model data is protected by Policies defined in Policy classes stored in the app/policy directory. The following file creates a basic "wide open" set of policies for development mode. You will then need to add specific Policies to protect your data in production mode.
# app/policies/application_policy.rb
# Policies regulate access to your public models
# The following policy will open up full access (but only in development)
# The policy system is very flexible and powerful. See the documentation
# for complete details.
class Hyperstack::ApplicationPolicy
# Allow any session to connect:
always_allow_connection
# Send all attributes from all public models
regulate_all_broadcasts { |policy| policy.send_all }
# Allow all changes to models
allow_change(to: :all, on: [:create, :update, :destroy]) { true }
# Allow remote access to all scopes - i.e. you can count or get a list of ids
# for any scope or relationship
# You can also add the line `regulate_scope :all` directly to your
# ApplicationRecord class.
ApplicationRecord.regulate_scope :all
end unless Rails.env.production?Note that regardless of whether models are public (i.e stored in the hyperstack/models directory) or private, they are ultimately protected by the Policy system.
Add the following file to the config/initializers/ directory:
# config/initializers/hyperstack.rb
Hyperstack.configuration do |config|
# If you do not want to use ActionCable,
# see http://hyperstack.orgs/docs/models/configuring-transport/
# for setting up other options.
config.transport = :action_cable # or :pusher or :simple_poller
# typically you will want to develop with prerendering off, and
# once the system is working, turn it on for final debug and test.
config.prerendering = :off # or :on
# We bring in React and ReactRouter via Yarn/Webpacker
config.cancel_import 'react/react-source-browser'
# remove the following line if you don't need jquery (see notes in application.js)
config.import 'hyperstack/component/jquery', client_only: true
# remove this line if you don't want to use the hotloader
config.import 'hyperstack/hotloader', client_only: true if Rails.env.development?
end
# useful for debugging
module Hyperstack
def self.on_error(operation, err, params, formatted_error_message)
::Rails.logger.debug(
"\#{formatted_error_message}\\n\\n" +
Pastel.new.red(
'To further investigate you may want to add a debugging '\\
'breakpoint to the on_error method in config/initializers/hyperstack.rb'
)
)
end
end if Rails.env.development?The first section configures Hyperstack and the assets that will be included (or not included) in the asset manifest.
The on_error method defines what you want to do when errors occur. In production you will may want to direct the output to a dedicated log file for example.
At the beginning of the config/routes.rb file mount the Hyperstack engine:
# config/routes.rb
Rails.application.routes.draw do
# this route should be first in the routes file so it always matches
mount Hyperstack::Engine => '/hyperstack'
# the rest of your routes here
endYou can mount the engine under any name you please. All of internal Hyperstack requests will be prefixed with whatever name you use.
Note: You can also directly ask Hyperstack to mount your top level components via the routes file. For example
Rails.application.routes.draw do mount Hyperstack::Engine => '/hyperstack' # this must be the first route get '/(*other)', to: 'hyperstack#app'will pass all requests (i.e.
/(*other)) to the hyperstack engine, and find and mount a component namedApp. Whatever you name the engine mount point (i.e.hyperstackin this case) is what you direct the requests to.Likewise
get /price-quote/(*other), to: hyperstack#price_quotewould mount a component namedPriceQuotewhen the url begins withprice-quote.Remember though that the first route that matches will be used, so if you had both examples in your routes, the price-quote route would be before the wildcard route.
This the way you have have 2 or more single page apps served by the same Rails backend.
If you are using the Hotloader, then you will also want to use the foreman gem. The Hotloader runs in its own application process, and foreman will start and stop both Rails and the Hotloader together.
The foreman gem is configured by a Procfile at the root of your application:
web: bundle exec rails s -b 0.0.0.0
hot-loader: bundle exec hyperstack-hotloader -p 25222 -d app/hyperstack
This instructs foreman to start rails in one process, and the Hotloader in a second process. -p 25222 is the port Hotloader will use and -d app/hyperstack is the directory that will be watched for changes.
To run foreman simply execute bundle exec foreman start, and CTRL-C to quit.
Note when running foreman with the above Procfile your port will be 5000 instead of the usual 3000.
No matter which way you installed Hyperstack you can use the included generators to add new components.
bundle exec rails g hyper:router App will create a skeleton top level (router) component named App.
bundle exec rails g hyper:component Index will create a skeleton component named Index.
Once you have installed Hyperstack you have a couple of options to see how things work.
Adding a top level router
The easiest way to make sure everything is installed okay is to use the generator to add an App router
bundle exec rails g hyper:router App
and then route everything to this component from your routes file:
get '/(*other)', to: 'hyperstack#app'.
Mounting a component from an existing page
Another approach is to add a simple component using the component generator:
bundle exec rails g hyper:component HyperTest
and then mount this component using ERB from within an existing view:
<% render_component 'HyperTest' %>