├── dist
└── .keep
├── test
├── dummy
│ ├── log
│ │ └── .keep
│ ├── lib
│ │ └── assets
│ │ │ └── .keep
│ ├── public
│ │ ├── favicon.ico
│ │ ├── apple-touch-icon.png
│ │ ├── apple-touch-icon-precomposed.png
│ │ ├── 500.html
│ │ ├── 422.html
│ │ └── 404.html
│ ├── .ruby-version
│ ├── app
│ │ ├── assets
│ │ │ ├── images
│ │ │ │ └── .keep
│ │ │ ├── config
│ │ │ │ └── manifest.js
│ │ │ └── stylesheets
│ │ │ │ ├── posts.css
│ │ │ │ ├── application.css
│ │ │ │ └── scaffold.css
│ │ ├── models
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ ├── post.rb
│ │ │ ├── action_item.rb
│ │ │ └── application_record.rb
│ │ ├── controllers
│ │ │ ├── concerns
│ │ │ │ └── .keep
│ │ │ ├── application_controller.rb
│ │ │ ├── home_controller.rb
│ │ │ └── posts_controller.rb
│ │ ├── helpers
│ │ │ ├── posts_helper.rb
│ │ │ └── application_helper.rb
│ │ ├── views
│ │ │ ├── posts
│ │ │ │ ├── _post.html.erb
│ │ │ │ ├── new.html.erb
│ │ │ │ ├── _card.html.erb
│ │ │ │ ├── edit.html.erb
│ │ │ │ ├── show.html.erb
│ │ │ │ ├── _form.html.erb
│ │ │ │ └── index.html.erb
│ │ │ └── layouts
│ │ │ │ └── application.html.erb
│ │ ├── channels
│ │ │ └── application_cable
│ │ │ │ ├── channel.rb
│ │ │ │ └── connection.rb
│ │ ├── jobs
│ │ │ └── application_job.rb
│ │ └── javascript
│ │ │ └── packs
│ │ │ └── application.js
│ ├── db
│ │ ├── test.sqlite3
│ │ ├── development.sqlite3
│ │ ├── migrate
│ │ │ ├── 20200711122838_create_posts.rb
│ │ │ └── 2021042923813_create_action_items.rb
│ │ └── schema.rb
│ ├── bin
│ │ ├── rake
│ │ ├── rails
│ │ └── setup
│ ├── config
│ │ ├── spring.rb
│ │ ├── environment.rb
│ │ ├── initializers
│ │ │ ├── mime_types.rb
│ │ │ ├── filter_parameter_logging.rb
│ │ │ ├── application_controller_renderer.rb
│ │ │ ├── cookies_serializer.rb
│ │ │ ├── wrap_parameters.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── inflections.rb
│ │ │ └── content_security_policy.rb
│ │ ├── cable.yml
│ │ ├── boot.rb
│ │ ├── routes.rb
│ │ ├── database.yml
│ │ ├── locales
│ │ │ └── en.yml
│ │ ├── application.rb
│ │ ├── environments
│ │ │ ├── development.rb
│ │ │ ├── test.rb
│ │ │ └── production.rb
│ │ └── puma.rb
│ ├── config.ru
│ └── Rakefile
├── integration
│ └── navigation_test.rb
├── support
│ └── helpers.rb
├── test_helper.rb
├── resolver
│ ├── controller_test.rb
│ └── controller
│ │ ├── instrumentation_test.rb
│ │ └── renderer_test.rb
├── futurism_test.rb
├── helper
│ └── helper_test.rb
└── cable
│ └── channel_test.rb
├── app
├── assets
│ └── javascripts
│ │ └── .keep
└── channels
│ └── futurism
│ └── channel.rb
├── .github
├── FUNDING.yml
├── PULL_REQUEST_TEMPLATE.md
├── workflows
│ ├── prettier-standard.yml
│ ├── standardrb.yml
│ └── tests.yml
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── .standard.yml
├── config
└── routes.rb
├── lib
├── futurism
│ ├── version.rb
│ ├── importmap.rb
│ ├── message_verifier.rb
│ ├── resolver
│ │ ├── controller.rb
│ │ ├── controller
│ │ │ ├── instrumentation.rb
│ │ │ └── renderer.rb
│ │ └── resources.rb
│ ├── configuration.rb
│ ├── options_transformer.rb
│ ├── engine.rb
│ ├── shims
│ │ └── deep_transform_values.rb
│ └── helpers.rb
├── futurism.rb
└── tasks
│ └── futurism_tasks.rake
├── bin
├── standardize
├── test
└── rails
├── javascript
├── index.js
├── elements
│ ├── futurism_element.js
│ ├── futurism_li.js
│ ├── futurism_table_row.js
│ ├── futurism_utils.js
│ └── index.js
├── utils
│ └── crypto.js
└── futurism_channel.js
├── gemfiles
├── rails_6_0.gemfile
├── rails_6_1.gemfile
├── rails_7_0.gemfile
├── rails_7_1.gemfile
└── rails_5_2.gemfile
├── .dir-locals.el
├── .gitignore
├── Appraisals
├── Gemfile
├── Rakefile
├── MIT-LICENSE
├── futurism.gemspec
├── package.json
├── rollup.config.js
├── .all-contributorsrc
├── Gemfile.lock
├── README.md
└── CHANGELOG.md
/dist/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/javascripts/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/.ruby-version:
--------------------------------------------------------------------------------
1 | 3.3.0
2 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: julianrubisch
2 |
--------------------------------------------------------------------------------
/.standard.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 | - "test/dummy/**/*"
3 |
--------------------------------------------------------------------------------
/test/dummy/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/helpers/posts_helper.rb:
--------------------------------------------------------------------------------
1 | module PostsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/models/post.rb:
--------------------------------------------------------------------------------
1 | class Post < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/lib/futurism/version.rb:
--------------------------------------------------------------------------------
1 | module Futurism
2 | VERSION = "1.4.2"
3 | end
4 |
--------------------------------------------------------------------------------
/test/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/models/action_item.rb:
--------------------------------------------------------------------------------
1 | class ActionItem < ApplicationRecord
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/app/views/posts/_post.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= post.title %>
3 | |
4 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 |
--------------------------------------------------------------------------------
/test/dummy/db/test.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stimulusreflex/futurism/HEAD/test/dummy/db/test.sqlite3
--------------------------------------------------------------------------------
/test/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | end
3 |
--------------------------------------------------------------------------------
/test/dummy/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative "../config/boot"
3 | require "rake"
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/test/dummy/db/development.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stimulusreflex/futurism/HEAD/test/dummy/db/development.sqlite3
--------------------------------------------------------------------------------
/bin/standardize:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | bundle exec standardrb --fix
4 |
5 | npx prettier-standard lib/templates/**/*.js
6 |
--------------------------------------------------------------------------------
/test/dummy/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/bin/test:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | $: << File.expand_path("../test", __dir__)
3 |
4 | require "bundler/setup"
5 | require "rails/plugin/test"
6 |
--------------------------------------------------------------------------------
/lib/futurism/importmap.rb:
--------------------------------------------------------------------------------
1 | pin "cable_ready", to: "cable_ready.min.js", preload: true
2 | pin "futurism", to: "futurism.min.js", preload: true
3 |
--------------------------------------------------------------------------------
/test/dummy/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/views/posts/new.html.erb:
--------------------------------------------------------------------------------
1 | New Post
2 |
3 | <%= render 'form', post: @post %>
4 |
5 | <%= link_to 'Back', posts_path %>
6 |
--------------------------------------------------------------------------------
/test/dummy/config/spring.rb:
--------------------------------------------------------------------------------
1 | Spring.watch(
2 | ".ruby-version",
3 | ".rbenv-vars",
4 | "tmp/restart.txt",
5 | "tmp/caching-dev.txt"
6 | )
7 |
--------------------------------------------------------------------------------
/test/dummy/app/views/posts/_card.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= post.title %>
3 | <%= link_to("Edit", edit_post_path(post)) %>
4 |
5 |
--------------------------------------------------------------------------------
/test/dummy/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/home_controller.rb:
--------------------------------------------------------------------------------
1 | class HomeController < ApplicationController
2 | # GET /home
3 | def index
4 | head :no_content
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/test/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 |
--------------------------------------------------------------------------------
/test/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path("../config/application", __dir__)
3 | require_relative "../config/boot"
4 | require "rails/commands"
5 |
--------------------------------------------------------------------------------
/test/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/stylesheets/posts.css:
--------------------------------------------------------------------------------
1 | /*
2 | Place all the styles related to the matching controller here.
3 | They will automatically be included in application.css.
4 | */
5 |
--------------------------------------------------------------------------------
/test/dummy/app/views/posts/edit.html.erb:
--------------------------------------------------------------------------------
1 | Editing Post
2 |
3 | <%= render 'form', post: @post %>
4 |
5 | <%= link_to 'Show', @post %> |
6 | <%= link_to 'Back', posts_path %>
7 |
--------------------------------------------------------------------------------
/javascript/index.js:
--------------------------------------------------------------------------------
1 | import { createSubscription } from './futurism_channel'
2 | import { initializeElements } from './elements'
3 |
4 | export { createSubscription, initializeElements }
5 |
--------------------------------------------------------------------------------
/gemfiles/rails_6_0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "~> 6.0"
6 | gem "sqlite3", "~> 1.4"
7 |
8 | gemspec path: "../"
9 |
--------------------------------------------------------------------------------
/gemfiles/rails_6_1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "~> 6.1"
6 | gem "sqlite3", "~> 1.4"
7 |
8 | gemspec path: "../"
9 |
--------------------------------------------------------------------------------
/gemfiles/rails_7_0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "7.0.8"
6 | gem "sqlite3", "~> 1.4"
7 |
8 | gemspec path: "../"
9 |
--------------------------------------------------------------------------------
/gemfiles/rails_7_1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "rails", "7.1.3"
6 | gem "sqlite3", "~> 1.4"
7 |
8 | gemspec path: "../"
9 |
--------------------------------------------------------------------------------
/test/integration/navigation_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class NavigationTest < ActionDispatch::IntegrationTest
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/dummy/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 |
--------------------------------------------------------------------------------
/.dir-locals.el:
--------------------------------------------------------------------------------
1 | ;;; Directory Local Variables
2 | ;;; For more information see (info "(emacs) Directory Variables")
3 |
4 | ((nil
5 | (prettier-js-command . "prettier-standard"))
6 | (ruby-mode
7 | (flycheck-checker . ruby-standard)))
8 |
9 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/20200711122838_create_posts.rb:
--------------------------------------------------------------------------------
1 | class CreatePosts < ActiveRecord::Migration[6.0]
2 | def change
3 | create_table :posts do |t|
4 | t.string :title
5 |
6 | t.timestamps
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/dummy/app/views/posts/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= notice %>
2 |
3 |
4 | Title:
5 | <%= @post.title %>
6 |
7 |
8 | <%= link_to 'Edit', edit_post_path(@post) %> |
9 | <%= link_to 'Back', posts_path %>
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .bundle/
3 | log/*.log
4 | pkg/
5 | test/dummy/log/*.log
6 | test/dummy/tmp/
7 | *~
8 | node_modules
9 | gemfiles/*.lock
10 |
11 | dist/**
12 | !dist/.keep
13 |
14 | app/assets/javascripts/**
15 | !app/assets/javascripts/.keep
--------------------------------------------------------------------------------
/test/dummy/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10 | channel_prefix: dummy_production
11 |
--------------------------------------------------------------------------------
/test/dummy/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 |
--------------------------------------------------------------------------------
/test/dummy/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_relative "config/application"
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/test/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # Set up gems listed in the Gemfile.
2 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__)
3 |
4 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
5 | $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__)
6 |
--------------------------------------------------------------------------------
/test/dummy/db/migrate/2021042923813_create_action_items.rb:
--------------------------------------------------------------------------------
1 | class CreateActionItems < ActiveRecord::Migration[6.0]
2 | def change
3 | create_table :action_items do |t|
4 | t.string :description
5 |
6 | t.timestamps
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ActiveSupport::Reloader.to_prepare do
4 | # ApplicationController.renderer.defaults.merge!(
5 | # http_host: 'example.org',
6 | # https: false
7 | # )
8 | # end
9 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Specify a serializer for the signed and encrypted cookie jars.
4 | # Valid options are :json, :marshal, and :hybrid.
5 | Rails.application.config.action_dispatch.cookies_serializer = :json
6 |
--------------------------------------------------------------------------------
/test/dummy/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | # Automatically retry jobs that encountered a deadlock
3 | # retry_on ActiveRecord::Deadlocked
4 |
5 | # Most jobs are safe to ignore if the underlying records are no longer available
6 | # discard_on ActiveJob::DeserializationError
7 | end
8 |
--------------------------------------------------------------------------------
/gemfiles/rails_5_2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", branch: "fix-bundle-env", git: "https://github.com/excid3/appraisal.git"
6 | gem "rails", "~> 5.2"
7 | gem "sqlite3", "~> 1.3", "< 1.4"
8 | gem "action-cable-testing"
9 |
10 | gemspec path: "../"
11 |
--------------------------------------------------------------------------------
/test/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 | <%= csrf_meta_tags %>
6 | <%= csp_meta_tag %>
7 |
8 | <%= stylesheet_link_tag 'application', media: 'all' %>
9 |
10 |
11 |
12 | <%= yield %>
13 |
14 |
15 |
--------------------------------------------------------------------------------
/javascript/elements/futurism_element.js:
--------------------------------------------------------------------------------
1 | /* global HTMLElement */
2 |
3 | import {
4 | extendElementWithIntersectionObserver,
5 | extendElementWithEagerLoading
6 | } from './futurism_utils'
7 |
8 | export default class FuturismElement extends HTMLElement {
9 | connectedCallback () {
10 | extendElementWithIntersectionObserver(this)
11 | extendElementWithEagerLoading(this)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/javascript/elements/futurism_li.js:
--------------------------------------------------------------------------------
1 | /* global HTMLLIElement */
2 |
3 | import {
4 | extendElementWithIntersectionObserver,
5 | extendElementWithEagerLoading
6 | } from './futurism_utils'
7 |
8 | export default class FuturismLI extends HTMLLIElement {
9 | connectedCallback () {
10 | extendElementWithIntersectionObserver(this)
11 | extendElementWithEagerLoading(this)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/javascript/elements/futurism_table_row.js:
--------------------------------------------------------------------------------
1 | /* global HTMLTableRowElement */
2 |
3 | import {
4 | extendElementWithIntersectionObserver,
5 | extendElementWithEagerLoading
6 | } from './futurism_utils'
7 |
8 | export default class FuturismTableRow extends HTMLTableRowElement {
9 | connectedCallback () {
10 | extendElementWithIntersectionObserver(this)
11 | extendElementWithEagerLoading(this)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/dummy/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]
9 | end
10 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # Type of PR (feature, enhancement, bug fix, etc.)
2 |
3 | ## Description
4 |
5 | Please include a summary of the change and which issue is fixed.
6 |
7 | Fixes # (issue)
8 |
9 | ## Why should this be added
10 |
11 | Explain value.
12 |
13 | ## Checklist
14 |
15 | - [ ] My code follows the style guidelines of this project
16 | - [ ] Checks (StandardRB & Prettier-Standard) are passing
17 |
--------------------------------------------------------------------------------
/lib/futurism/message_verifier.rb:
--------------------------------------------------------------------------------
1 | module Futurism
2 | module MessageVerifier
3 | def self.message_verifier
4 | @message_verifier ||= ActiveSupport::MessageVerifier.new(Rails.application.key_generator.generate_key("futurism/verifier_key"), digest: "SHA256", serializer: Marshal)
5 | end
6 |
7 | def message_verifier
8 | @message_verifier ||= Futurism::MessageVerifier.message_verifier
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/futurism/resolver/controller.rb:
--------------------------------------------------------------------------------
1 | module Futurism
2 | module Resolver
3 | class Controller
4 | def self.from(signed_string:)
5 | if signed_string.present?
6 | Futurism::MessageVerifier
7 | .message_verifier
8 | .verify(signed_string)
9 | .to_s
10 | .safe_constantize
11 | else
12 | Futurism.default_controller
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/dummy/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 |
--------------------------------------------------------------------------------
/lib/futurism/configuration.rb:
--------------------------------------------------------------------------------
1 | module Futurism
2 | class << self
3 | def configure
4 | yield configuration
5 | end
6 |
7 | def configuration
8 | @configuration ||= Configuration.new
9 | end
10 |
11 | alias_method :config, :configuration
12 | end
13 |
14 | class Configuration
15 | attr_accessor :parent_channel
16 |
17 | def initialize
18 | @parent_channel = "::ApplicationCable::Channel"
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/test/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :posts
3 | # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
4 |
5 | put "/known/get", to: "home#get_action"
6 | put "/known/put", to: "home#put_action"
7 | patch "/known/patch", to: "home#patch_action"
8 | delete "/known/delete", to: "home#delete_action"
9 | post "/known/post", to: "home#post_action"
10 |
11 | root "home#index"
12 | end
13 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | if RUBY_VERSION >= "2.7"
2 | appraise "rails-7-0" do
3 | gem "rails", "7.0.1"
4 | gem "sqlite3", "~> 1.4"
5 | end
6 | end
7 |
8 | appraise "rails-6-1" do
9 | gem "rails", "~> 6.1"
10 | gem "sqlite3", "~> 1.4"
11 | end
12 |
13 | appraise "rails-6-0" do
14 | gem "rails", "~> 6.0"
15 | gem "sqlite3", "~> 1.4"
16 | end
17 |
18 | if RUBY_VERSION < "3.0"
19 | appraise "rails-5-2" do
20 | gem "rails", "~> 5.2"
21 | gem "sqlite3", "~> 1.3", "< 1.4"
22 | gem "action-cable-testing"
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/javascript/utils/crypto.js:
--------------------------------------------------------------------------------
1 | /* global crypto */
2 |
3 | export async function sha256 (message) {
4 | // encode as UTF-8
5 | const msgBuffer = new TextEncoder('utf-8').encode(message)
6 |
7 | // hash the message
8 | const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer)
9 |
10 | // convert ArrayBuffer to Array
11 | const hashArray = Array.from(new Uint8Array(hashBuffer))
12 |
13 | // convert bytes to hex string
14 | const hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('')
15 |
16 | return hashHex
17 | }
18 |
--------------------------------------------------------------------------------
/test/support/helpers.rb:
--------------------------------------------------------------------------------
1 | require "active_support/test_case"
2 |
3 | class ActiveSupport::TestCase
4 | # Execute the block setting the given values and restoring old values after
5 | # the block is executed.
6 | def swap(object, new_values)
7 | old_values = {}
8 | new_values.each do |key, value|
9 | old_values[key] = object.public_send(key)
10 | object.public_send(:"#{key}=", value)
11 | end
12 | yield
13 | ensure
14 | old_values.each do |key, value|
15 | object.public_send(:"#{key}=", value)
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # Configure Rails Environment
2 | ENV["RAILS_ENV"] = "test"
3 |
4 | require_relative "../test/dummy/config/environment"
5 | require "rails/test_help"
6 | require "minitest/mock"
7 | require "nokogiri"
8 |
9 | # Load support files
10 | Dir["#{__dir__}/support/**/*.rb"].sort.each { |f| require f }
11 |
12 | # Filter out the backtrace from minitest while preserving the one from other libraries.
13 | Minitest.backtrace_filter = Minitest::BacktraceFilter.new
14 |
15 | # Turn off logger output as to not have poor test output
16 | Futurism.logger = Logger.new(IO::NULL)
17 |
--------------------------------------------------------------------------------
/.github/workflows/prettier-standard.yml:
--------------------------------------------------------------------------------
1 | name: Prettier-Standard
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - '*'
7 | push:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | prettier:
13 | name: Prettier-Standard Check Action
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@master
17 | - name: Setup Node
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: '12.x'
21 | - name: Run prettier-standard check
22 | run: npx prettier-standard --lint
23 | working-directory: javascript
24 |
--------------------------------------------------------------------------------
/test/dummy/app/views/posts/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_with(model: post, local: true) do |form| %>
2 | <% if post.errors.any? %>
3 |
4 |
<%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:
5 |
6 |
7 | <% post.errors.full_messages.each do |message| %>
8 | - <%= message %>
9 | <% end %>
10 |
11 |
12 | <% end %>
13 |
14 |
15 | <%= form.label :title %>
16 | <%= form.text_field :title %>
17 |
18 |
19 |
20 | <%= form.submit %>
21 |
22 | <% end %>
23 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | # Declare your gem's dependencies in futurism.gemspec.
5 | # Bundler will treat runtime dependencies like base dependencies, and
6 | # development dependencies will be added by default to the :development group.
7 | gemspec
8 |
9 | # Declare any dependencies that are still in development here instead of in
10 | # your gemspec. These might include edge Rails or gems from your path or
11 | # Git. Remember to move these dependencies to your gemspec before releasing
12 | # your gem to rubygems.org.
13 |
14 | # To use a debugger
15 | # gem 'byebug', group: [:development, :test]
16 |
--------------------------------------------------------------------------------
/test/dummy/app/views/posts/index.html.erb:
--------------------------------------------------------------------------------
1 | <%= notice %>
2 |
3 | Posts
4 |
5 |
6 |
7 |
8 | | Title |
9 | |
10 |
11 |
12 |
13 |
14 | <% @posts.each do |post| %>
15 |
16 | | <%= post.title %> |
17 | <%= link_to 'Show', post %> |
18 | <%= link_to 'Edit', edit_post_path(post) %> |
19 | <%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %> |
20 |
21 | <% end %>
22 |
23 |
24 |
25 |
26 |
27 | <%= link_to 'New Post', new_post_path %>
28 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | begin
2 | require "bundler/setup"
3 | rescue LoadError
4 | puts "You must `gem install bundler` and `bundle install` to run rake tasks"
5 | end
6 |
7 | require "rdoc/task"
8 |
9 | RDoc::Task.new(:rdoc) do |rdoc|
10 | rdoc.rdoc_dir = "rdoc"
11 | rdoc.title = "Futurism"
12 | rdoc.options << "--line-numbers"
13 | rdoc.rdoc_files.include("README.md")
14 | rdoc.rdoc_files.include("lib/**/*.rb")
15 | end
16 |
17 | load "rails/tasks/statistics.rake"
18 |
19 | require "bundler/gem_tasks"
20 |
21 | require "rake/testtask"
22 |
23 | Rake::TestTask.new(:test) do |t|
24 | t.libs << "test"
25 | t.pattern = "test/**/*_test.rb"
26 | t.verbose = false
27 | end
28 |
29 | task default: :test
30 |
--------------------------------------------------------------------------------
/test/dummy/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 |
--------------------------------------------------------------------------------
/test/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite. Versions 3.8.0 and up are supported.
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: <%= ENV.fetch("RAILS_MAX_THREADS") { 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 |
--------------------------------------------------------------------------------
/test/dummy/app/javascript/packs/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. JavaScript code in this file should be added after the last require_* statement.
9 | //
10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11 | // about supported directives.
12 | //
13 | //= require rails-ujs
14 | //= require_tree .
15 |
--------------------------------------------------------------------------------
/lib/futurism/options_transformer.rb:
--------------------------------------------------------------------------------
1 | module Futurism
2 | module OptionsTransformer
3 | def dump_options(options)
4 | require_relative "shims/deep_transform_values" unless options.respond_to? :deep_transform_values
5 |
6 | options.deep_transform_values do |value|
7 | next(value) unless value.respond_to?(:to_global_id)
8 | next(value) if value.is_a?(ActiveRecord::Base) && value.new_record?
9 |
10 | value.to_global_id.to_s
11 | end
12 | end
13 |
14 | def load_options(options)
15 | require_relative "shims/deep_transform_values" unless options.respond_to? :deep_transform_values
16 |
17 | options.deep_transform_values { |value| (value.is_a?(String) && value.start_with?("gid://")) ? GlobalID::Locator.locate(value) : value }
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/dummy/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 other CSS/SCSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | ---
5 |
6 | # Feature Request
7 |
8 | _Please help us help you by filling out any applicable information in this template and removing the rest!_
9 |
10 | ## Is your feature request related to a problem?
11 |
12 | A clear and concise description of what the problem is. If this is a bug, please open a bug report instead of a feature request!
13 |
14 | ## Describe the solution you'd like
15 |
16 | A clear and concise description of what you want to happen. Please also include alternative solutions or features you've considered.
17 |
18 | ## Additional context
19 |
20 | Add any other context or screenshots about the feature request here.
21 |
22 | ## PR link
23 |
24 | If you have opened a pull request to address the issue, please link it here.
25 |
--------------------------------------------------------------------------------
/.github/workflows/standardrb.yml:
--------------------------------------------------------------------------------
1 | name: StandardRB
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - '*'
7 | push:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | standard:
13 | name: StandardRB Check Action
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@master
17 | - name: Set up Ruby 3
18 | uses: ruby/setup-ruby@v1
19 | with:
20 | ruby-version: 3.0.3
21 | bundler-cache: true
22 | - name: Install sqlite headers
23 | run: |
24 | sudo apt-get update
25 | sudo apt-get install libsqlite3-dev
26 | - name: Bundle install
27 | run: |
28 | gem install bundler
29 | bundle config path vendor/bundle
30 | bundle install --jobs 4 --retry 3
31 | - name: Run StandardRB
32 | run: bundle exec standardrb --format progress
33 |
--------------------------------------------------------------------------------
/test/dummy/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path("..", __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to setup or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts "== Installing dependencies =="
17 | system! "gem install bundler --conservative"
18 | system("bundle check") || system!("bundle install")
19 |
20 | puts "\n== Removing old logs and tempfiles =="
21 | system! "bin/rails log:clear tmp:clear"
22 |
23 | puts "\n== Restarting application server =="
24 | system! "bin/rails restart"
25 | end
26 |
--------------------------------------------------------------------------------
/lib/futurism/engine.rb:
--------------------------------------------------------------------------------
1 | module Futurism
2 | class Engine < ::Rails::Engine
3 | initializer "futurism.assets" do |app|
4 | if app.config.respond_to?(:assets)
5 | app.config.assets.precompile += %w[
6 | futurism.js
7 | futurism.min.js
8 | futurism.min.js.map
9 | futurism.umd.js
10 | futurism.umd.min.js
11 | futurism.umd.min.js.map
12 | ]
13 | end
14 | end
15 |
16 | initializer "futurism.importmap", before: "importmap" do |app|
17 | if app.config.respond_to?(:importmap)
18 | app.config.importmap.paths << Engine.root.join("lib/futurism/importmap.rb")
19 | app.config.importmap.cache_sweepers << Engine.root.join("app/assets/javascripts")
20 | end
21 | end
22 |
23 | initializer "futurism.logger", after: "initialize_logger" do
24 | Futurism.logger ||= Rails.logger || Logger.new($stdout)
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails gems
3 | # installed from the root of your application.
4 |
5 | ENGINE_ROOT = File.expand_path('..', __dir__)
6 | ENGINE_PATH = File.expand_path('../lib/futurism/engine', __dir__)
7 | APP_PATH = File.expand_path('../test/dummy/config/application', __dir__)
8 |
9 | # Set up gems listed in the Gemfile.
10 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
11 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
12 |
13 | require "rails"
14 | # Pick the frameworks you want:
15 | require "active_model/railtie"
16 | require "active_job/railtie"
17 | # require "active_record/railtie"
18 | # require "active_storage/engine"
19 | require "action_controller/railtie"
20 | # require "action_mailer/railtie"
21 | require "action_view/railtie"
22 | require "action_cable/engine"
23 | # require "sprockets/railtie"
24 | require "rails/test_unit/railtie"
25 | require 'rails/engine/commands'
26 |
--------------------------------------------------------------------------------
/test/dummy/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 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/test/resolver/controller_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class DummyController < ActionController::Base; end
4 |
5 | class Futurism::Resolver::ControllerTest < ActiveSupport::TestCase
6 | test ".from defaults to ApplicationController" do
7 | controller = Futurism::Resolver::Controller.from(signed_string: nil)
8 | assert_equal controller, ApplicationController
9 | end
10 |
11 | test ".from uses Futurism.default_controller" do
12 | swap Futurism, default_controller: DummyController do
13 | controller = Futurism::Resolver::Controller.from(signed_string: nil)
14 |
15 | assert_equal controller, DummyController
16 | end
17 | end
18 |
19 | test ".from lookups up controller via signed_string:" do
20 | signed_controller_string = Futurism::MessageVerifier.message_verifier.generate(DummyController.to_s)
21 | controller = Futurism::Resolver::Controller.from(signed_string: signed_controller_string)
22 |
23 | assert_equal controller, DummyController
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | ---
5 |
6 | # Bug Report
7 |
8 | _Please help us help you by filling out any applicable information in this template and removing the rest!_
9 |
10 | ## Describe the bug
11 |
12 | A clear and concise description of what the bug is.
13 |
14 | ## To Reproduce
15 |
16 | Steps to reproduce the behavior
17 |
18 | ## Expected behavior
19 |
20 | A clear and concise description of what you expected to happen.
21 |
22 | ## Screenshots or reproduction
23 |
24 | If applicable, add screenshots (errors, example of the behavior, etc.) to help explain your problem or post a link to a repository that replicates the issue.
25 |
26 | ## Versions
27 |
28 | ### Futurism
29 |
30 | - Gem: [e.g. 0.3.1]
31 | - Node package: [e.g. 0.3.1]
32 |
33 | ### External tools
34 |
35 | - Ruby: [e.g. 2.6.4]
36 | - Rails: [e.g. 6.0.0]
37 | - CableReady: [e.g. 4.3.0]
38 | - Node: [e.g. 12.4.0]
39 |
40 | ### Browser
41 |
42 | - Browser [e.g. chrome, safari]
43 | - Version [e.g. 22]
44 |
--------------------------------------------------------------------------------
/lib/futurism/resolver/controller/instrumentation.rb:
--------------------------------------------------------------------------------
1 | require "active_support/notifications"
2 |
3 | module Futurism
4 | module Resolver
5 | class Controller
6 | class Instrumentation < SimpleDelegator
7 | PARAMETERS_KEY = ActionDispatch::Http::Parameters::PARAMETERS_KEY
8 |
9 | def render(*args)
10 | ActiveSupport::Notifications.instrument(
11 | "render.futurism",
12 | channel: get_param(:channel),
13 | controller: get_param(:controller),
14 | action: get_param(:action),
15 | partial: extract_partial_name(*args)
16 | ) do
17 | super(*args)
18 | end
19 | end
20 |
21 | private
22 |
23 | def get_param(key)
24 | __getobj__.instance_variable_get(:@env).dig(PARAMETERS_KEY, key)
25 | end
26 |
27 | def extract_partial_name(opts_or_model, *args)
28 | opts_or_model.is_a?(Hash) ? opts_or_model[:partial] : opts_or_model.to_partial_path
29 | end
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/test/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "rails"
4 | # Pick the frameworks you want:
5 | require "active_model/railtie"
6 | require "active_job/railtie"
7 | require "active_record/railtie"
8 | # require "active_storage/engine"
9 | require "action_controller/railtie"
10 | # require "action_mailer/railtie"
11 | require "action_view/railtie"
12 | require "action_cable/engine"
13 | # require "sprockets/railtie"
14 | require "rails/test_unit/railtie"
15 |
16 | Bundler.require(*Rails.groups)
17 | require "futurism"
18 |
19 | module Dummy
20 | class Application < Rails::Application
21 | # Initialize configuration defaults for originally generated Rails version.
22 | config.load_defaults Rails::VERSION::MAJOR + (Rails::VERSION::MINOR / 10.0)
23 |
24 | # Settings in config/environments/* take precedence over those specified here.
25 | # Application configuration can go into files in config/initializers
26 | # -- all .rb files in that directory are automatically loaded after loading
27 | # the framework and any gems in your application.
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/test/futurism_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class DummyController < ActionController::Base; end
4 |
5 | class Futurism::Test < ActiveSupport::TestCase
6 | test "module" do
7 | assert_kind_of Module, Futurism
8 | end
9 |
10 | test ".skip_in_test?" do
11 | swap Futurism, skip_in_test: "" do
12 | assert_equal false, Futurism.skip_in_test?
13 | end
14 | end
15 |
16 | test ".instrumentation?" do
17 | swap Futurism, instrumentation: "" do
18 | assert_equal false, Futurism.instrumentation?
19 | end
20 | end
21 |
22 | test ".default_controller" do
23 | assert_equal ApplicationController, Futurism.default_controller
24 |
25 | swap Futurism, default_controller: nil do
26 | assert_equal ApplicationController, Futurism.default_controller
27 | end
28 |
29 | swap Futurism, default_controller: DummyController do
30 | assert_equal DummyController, Futurism.default_controller
31 | end
32 |
33 | swap Futurism, default_controller: "DummyController" do
34 | assert_equal DummyController, Futurism.default_controller
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Julian Rubisch
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/lib/futurism.rb:
--------------------------------------------------------------------------------
1 | require "rails"
2 |
3 | require "action_cable"
4 | require "cable_ready"
5 | require "futurism/configuration"
6 | require "futurism/engine"
7 | require "futurism/message_verifier"
8 | require "futurism/options_transformer"
9 | require "futurism/resolver/resources"
10 | require "futurism/resolver/controller"
11 | require "futurism/resolver/controller/renderer"
12 | require "futurism/resolver/controller/instrumentation"
13 | require "futurism/helpers"
14 |
15 | module Futurism
16 | extend ActiveSupport::Autoload
17 |
18 | autoload :Helpers, "futurism/helpers"
19 |
20 | mattr_accessor :skip_in_test, default: false
21 | mattr_accessor :instrumentation, default: false
22 | mattr_accessor :logger
23 |
24 | mattr_writer :default_controller
25 | def self.default_controller
26 | (@@default_controller || "::ApplicationController").to_s.constantize
27 | end
28 |
29 | def self.skip_in_test?
30 | skip_in_test.present?
31 | end
32 |
33 | def self.instrumentation?
34 | instrumentation.present?
35 | end
36 |
37 | ActiveSupport.on_load(:action_view) do
38 | include Futurism::Helpers
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/app/channels/futurism/channel.rb:
--------------------------------------------------------------------------------
1 | module Futurism
2 | class Channel < Futurism.configuration.parent_channel.constantize
3 | include CableReady::Broadcaster
4 |
5 | def stream_name
6 | ids = connection.identifiers.map { |identifier| send(identifier).try(:id) || send(identifier) }
7 | [
8 | params[:channel],
9 | ids.select(&:present?).join(";")
10 | ].select(&:present?).join(":")
11 | end
12 |
13 | def subscribed
14 | stream_from stream_name
15 | end
16 |
17 | def receive(data)
18 | resources = data.fetch_values("signed_params", "sgids", "signed_controllers", "urls", "broadcast_each") { |_key| Array.new(data["signed_params"].length, nil) }.transpose
19 |
20 | resolver = Resolver::Resources.new(resource_definitions: resources, connection: connection, params: @params)
21 | resolver.resolve do |selector, html, broadcast_each|
22 | cable_ready[stream_name].outer_html(
23 | selector: selector,
24 | html: html
25 | )
26 |
27 | cable_ready.broadcast if broadcast_each
28 | end
29 |
30 | cable_ready.broadcast
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/test/dummy/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # This file is the source Rails uses to define your schema when running `bin/rails
6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7 | # be faster and is potentially less error prone than running all of your
8 | # migrations from scratch. Old migrations may fail to apply correctly if those
9 | # migrations use external dependencies or application code.
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(version: 2020_07_11_122838) do
14 |
15 | create_table "action_items", force: :cascade do |t|
16 | t.string "description"
17 | t.datetime "created_at", precision: 6, null: false
18 | t.datetime "updated_at", precision: 6, null: false
19 | end
20 |
21 | create_table "posts", force: :cascade do |t|
22 | t.string "title"
23 | t.datetime "created_at", precision: 6, null: false
24 | t.datetime "updated_at", precision: 6, null: false
25 | end
26 |
27 | end
28 |
--------------------------------------------------------------------------------
/futurism.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path("lib", __dir__)
2 |
3 | # Maintain your gem's version:
4 | require "futurism/version"
5 |
6 | # Describe your gem and declare its dependencies:
7 | Gem::Specification.new do |spec|
8 | spec.name = "futurism"
9 | spec.version = Futurism::VERSION
10 | spec.authors = ["Julian Rubisch"]
11 | spec.email = ["julian@julianrubisch.at"]
12 | spec.homepage = "https://github.com/stimulusreflex/futurism"
13 | spec.summary = "Lazy-load Rails partials via CableReady"
14 | spec.description = "Uses custom html elements with attached IntersectionObserver to automatically lazy load partials via websockets"
15 | spec.license = "MIT"
16 |
17 | spec.files = Dir[
18 | "lib/**/*.rb",
19 | "lib/**/*.rake",
20 | "app/**/*.rb",
21 | "app/assets/javascripts/*",
22 | "bin/*",
23 | "[A-Z]*"
24 | ]
25 |
26 | spec.add_development_dependency "appraisal"
27 | spec.add_development_dependency "bundler", "~> 2.0"
28 | spec.add_development_dependency "rake", "~> 13.0"
29 | spec.add_development_dependency "nokogiri"
30 | spec.add_development_dependency "standardrb"
31 | spec.add_development_dependency "sqlite3"
32 |
33 | spec.add_dependency "rack", ">= 2", "< 4"
34 |
35 | spec.add_dependency "rails", ">= 5.2"
36 | spec.add_dependency "cable_ready", ">= 5.0"
37 | end
38 |
--------------------------------------------------------------------------------
/test/dummy/app/controllers/posts_controller.rb:
--------------------------------------------------------------------------------
1 | class PostsController < ApplicationController
2 | before_action :set_post, only: [:show, :edit, :update, :destroy]
3 |
4 | # GET /posts
5 | def index
6 | @posts = Post.all
7 | end
8 |
9 | # GET /posts/1
10 | def show
11 | end
12 |
13 | # GET /posts/new
14 | def new
15 | @post = Post.new
16 | end
17 |
18 | # GET /posts/1/edit
19 | def edit
20 | end
21 |
22 | # POST /posts
23 | def create
24 | @post = Post.new(post_params)
25 |
26 | if @post.save
27 | redirect_to @post, notice: "Post was successfully created."
28 | else
29 | render :new
30 | end
31 | end
32 |
33 | # PATCH/PUT /posts/1
34 | def update
35 | if @post.update(post_params)
36 | redirect_to @post, notice: "Post was successfully updated."
37 | else
38 | render :edit
39 | end
40 | end
41 |
42 | # DELETE /posts/1
43 | def destroy
44 | @post.destroy
45 | redirect_to posts_url, notice: "Post was successfully destroyed."
46 | end
47 |
48 | private
49 |
50 | # Use callbacks to share common setup or constraints between actions.
51 | def set_post
52 | @post = Post.find(params[:id])
53 | end
54 |
55 | # Only allow a trusted parameter "white list" through.
56 | def post_params
57 | params.require(:post).permit(:title)
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/test/dummy/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy
4 | # For further information see the following documentation
5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
6 |
7 | # Rails.application.config.content_security_policy do |policy|
8 | # policy.default_src :self, :https
9 | # policy.font_src :self, :https, :data
10 | # policy.img_src :self, :https, :data
11 | # policy.object_src :none
12 | # policy.script_src :self, :https
13 | # policy.style_src :self, :https
14 |
15 | # # Specify URI for violation reports
16 | # # policy.report_uri "/csp-violation-report-endpoint"
17 | # end
18 |
19 | # If you are using UJS then enable automatic nonce generation
20 | # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
21 |
22 | # Set the nonce only to specific directives
23 | # Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
24 |
25 | # Report CSP violations to a specified URI
26 | # For further information see the following documentation:
27 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
28 | # Rails.application.config.content_security_policy_report_only = true
29 |
--------------------------------------------------------------------------------
/test/dummy/app/assets/stylesheets/scaffold.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #fff;
3 | color: #333;
4 | margin: 33px;
5 | }
6 |
7 | body, p, ol, ul, td {
8 | font-family: verdana, arial, helvetica, sans-serif;
9 | font-size: 13px;
10 | line-height: 18px;
11 | }
12 |
13 | pre {
14 | background-color: #eee;
15 | padding: 10px;
16 | font-size: 11px;
17 | }
18 |
19 | a {
20 | color: #000;
21 | }
22 |
23 | a:visited {
24 | color: #666;
25 | }
26 |
27 | a:hover {
28 | color: #fff;
29 | background-color: #000;
30 | }
31 |
32 | th {
33 | padding-bottom: 5px;
34 | }
35 |
36 | td {
37 | padding: 0 5px 7px;
38 | }
39 |
40 | div.field,
41 | div.actions {
42 | margin-bottom: 10px;
43 | }
44 |
45 | #notice {
46 | color: green;
47 | }
48 |
49 | .field_with_errors {
50 | padding: 2px;
51 | background-color: red;
52 | display: table;
53 | }
54 |
55 | #error_explanation {
56 | width: 450px;
57 | border: 2px solid red;
58 | padding: 7px 7px 0;
59 | margin-bottom: 20px;
60 | background-color: #f0f0f0;
61 | }
62 |
63 | #error_explanation h2 {
64 | text-align: left;
65 | font-weight: bold;
66 | padding: 5px 5px 5px 15px;
67 | font-size: 12px;
68 | margin: -7px -7px 0;
69 | background-color: #c00;
70 | color: #fff;
71 | }
72 |
73 | #error_explanation ul li {
74 | font-size: 12px;
75 | list-style: square;
76 | }
77 |
78 | label {
79 | display: block;
80 | }
81 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - '*'
7 | push:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | ruby_test:
13 | name: Ruby Test Action
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | ruby-version: ['3.1.4', '3.2.2', '3.3.0']
18 | rails-version: ['6_1', '7_0', '7_1']
19 | env:
20 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/rails_${{ matrix.rails-version }}.gemfile
21 | steps:
22 | - uses: actions/checkout@master
23 | - name: Set up Ruby ${{ matrix.ruby-version }}
24 | uses: ruby/setup-ruby@v1
25 | with:
26 | ruby-version: ${{ matrix.ruby-version }}
27 | - uses: actions/cache@v1
28 | with:
29 | path: vendor/bundle
30 | key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
31 | restore-keys: |
32 | ${{ runner.os }}-gems-
33 | - name: Install sqlite headers
34 | run: |
35 | sudo apt-get update
36 | sudo apt-get install libsqlite3-dev
37 | - name: Bundle install
38 | run: |
39 | gem install bundler
40 | gem update --system
41 | bundle config path vendor/bundle
42 | bundle install --jobs 4 --retry 3
43 | - name: Run ruby tests
44 | run: |
45 | bundle exec rake test
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@stimulus_reflex/futurism",
3 | "version": "1.4.2",
4 | "description": "Lazy-load Rails partials via CableReady",
5 | "main": "./dist/futurism.umd.min.js",
6 | "module": "./dist/futurism.min.js",
7 | "files": [
8 | "dist/*",
9 | "javascript/*"
10 | ],
11 | "scripts": {
12 | "test": "yarn run mocha",
13 | "lint": "yarn run prettier-standard:check",
14 | "format": "yarn run prettier-standard:format",
15 | "prettier-standard:check": "yarn run prettier-standard --check ./javascript/**/*.js rollup.config.js",
16 | "prettier-standard:format": "yarn run prettier-standard ./javascript/**/*.js rollup.config.js",
17 | "build": "yarn rollup -c",
18 | "watch": "yarn rollup -wc"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/stimulusreflex/futurism.git"
23 | },
24 | "keywords": [
25 | "cable_ready",
26 | "lazy",
27 | "loading"
28 | ],
29 | "author": "Julian Rubisch ",
30 | "license": "MIT",
31 | "bugs": {
32 | "url": "https://github.com/stimulusreflex/futurism/issues"
33 | },
34 | "homepage": "https://github.com/stimulusreflex/futurism#readme",
35 | "dependencies": {
36 | "cable_ready": "^5.0.0"
37 | },
38 | "devDependencies": {
39 | "@rollup/plugin-commonjs": "^21.0.3",
40 | "@rollup/plugin-json": "^4.1.0",
41 | "@rollup/plugin-node-resolve": "^13.1.3",
42 | "mocha": "^8.0.1",
43 | "prettier-standard": "^16.4.1",
44 | "rollup": "^3.29.5",
45 | "rollup-plugin-terser": "^7.0.2"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lib/futurism/shims/deep_transform_values.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Hash
4 | # Returns a new hash with all values converted by the block operation.
5 | # This includes the values from the root hash and from all
6 | # nested hashes and arrays.
7 | #
8 | # hash = { person: { name: 'Rob', age: '28' } }
9 | #
10 | # hash.deep_transform_values{ |value| value.to_s.upcase }
11 | # # => {person: {name: "ROB", age: "28"}}
12 | def deep_transform_values(&block)
13 | _deep_transform_values_in_object(self, &block)
14 | end
15 |
16 | # Destructively converts all values by using the block operation.
17 | # This includes the values from the root hash and from all
18 | # nested hashes and arrays.
19 | def deep_transform_values!(&block)
20 | _deep_transform_values_in_object!(self, &block)
21 | end
22 |
23 | private
24 |
25 | # Support methods for deep transforming nested hashes and arrays.
26 | def _deep_transform_values_in_object(object, &block)
27 | case object
28 | when Hash
29 | object.transform_values { |value| _deep_transform_values_in_object(value, &block) }
30 | when Array
31 | object.map { |e| _deep_transform_values_in_object(e, &block) }
32 | else
33 | yield(object)
34 | end
35 | end
36 |
37 | def _deep_transform_values_in_object!(object, &block)
38 | case object
39 | when Hash
40 | object.transform_values! { |value| _deep_transform_values_in_object!(value, &block) }
41 | when Array
42 | object.map! { |e| _deep_transform_values_in_object!(e, &block) }
43 | else
44 | yield(object)
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports.
13 | config.consider_all_requests_local = true
14 |
15 | # Enable/disable caching. By default caching is disabled.
16 | # Run rails dev:cache to toggle caching.
17 | if Rails.root.join("tmp", "caching-dev.txt").exist?
18 | config.action_controller.perform_caching = true
19 | config.action_controller.enable_fragment_cache_logging = true
20 |
21 | config.cache_store = :memory_store
22 | config.public_file_server.headers = {
23 | "Cache-Control" => "public, max-age=#{2.days.to_i}"
24 | }
25 | else
26 | config.action_controller.perform_caching = false
27 |
28 | config.cache_store = :null_store
29 | end
30 |
31 | # Print deprecation notices to the Rails logger.
32 | config.active_support.deprecation = :log
33 |
34 | # Raises error for missing translations.
35 | # config.action_view.raise_on_missing_translations = true
36 |
37 | # Use an evented file watcher to asynchronously detect changes in source code,
38 | # routes, locales, etc. This feature depends on the listen gem.
39 | # config.file_watcher = ActiveSupport::EventedFileUpdateChecker
40 | end
41 |
--------------------------------------------------------------------------------
/javascript/futurism_channel.js:
--------------------------------------------------------------------------------
1 | /* global CustomEvent, setTimeout */
2 |
3 | import CableReady from 'cable_ready'
4 |
5 | const debounceEvents = (callback, delay = 20) => {
6 | let timeoutId
7 | let events = []
8 | return (...args) => {
9 | clearTimeout(timeoutId)
10 | events = [...events, ...args]
11 | timeoutId = setTimeout(() => {
12 | timeoutId = null
13 | callback(events)
14 | events = []
15 | }, delay)
16 | }
17 | }
18 |
19 | export const createSubscription = consumer => {
20 | consumer.subscriptions.create('Futurism::Channel', {
21 | connected () {
22 | window.Futurism = {
23 | subscription: this
24 | }
25 | document.addEventListener(
26 | 'futurism:appear',
27 | debounceEvents(events => {
28 | this.send({
29 | signed_params: events.map(e => e.target.dataset.signedParams),
30 | sgids: events.map(e => e.target.dataset.sgid),
31 | signed_controllers: events.map(
32 | e => e.target.dataset.signedController
33 | ),
34 | urls: events.map(_ => window.location.href),
35 | broadcast_each: events.map(e => e.target.dataset.broadcastEach)
36 | })
37 | })
38 | )
39 | },
40 |
41 | received (data) {
42 | if (data.cableReady) {
43 | CableReady.perform(data.operations, {
44 | emitMissingElementWarnings: false
45 | })
46 |
47 | document.dispatchEvent(
48 | new CustomEvent('futurism:appeared', {
49 | bubbles: true,
50 | cancelable: true
51 | })
52 | )
53 | }
54 | }
55 | })
56 | }
57 |
--------------------------------------------------------------------------------
/lib/tasks/futurism_tasks.rake:
--------------------------------------------------------------------------------
1 | require "fileutils"
2 |
3 | namespace :futurism do
4 | desc "Let's look into a brighter future with futurism and CableReady"
5 | task install: :environment do
6 | system "yarn add @stimulus_reflex/futurism"
7 |
8 | filepath = %w[
9 | app/javascript/channels/index.js
10 | app/javascript/channels/index.ts
11 | app/javascript/application.js
12 | app/javascript/application.ts
13 | app/javascript/packs/application.js
14 | app/javascript/packs/application.ts
15 | ]
16 | .select { |path| File.exist?(path) }
17 | .map { |path| Rails.root.join(path) }
18 | .first
19 |
20 | puts "Updating #{filepath}"
21 | lines = File.open(filepath, "r") { |f| f.readlines }
22 |
23 | unless lines.find { |line| line.start_with?("import * as Futurism") }
24 | matches = lines.select { |line| line =~ /\A(require|import)/ }
25 | lines.insert lines.index(matches.last).to_i + 1, "import * as Futurism from '@stimulus_reflex/futurism'\n"
26 | end
27 |
28 | unless lines.find { |line| line.start_with?("import consumer") }
29 | matches = lines.select { |line| line =~ /\A(require|import)/ }
30 | lines.insert lines.index(matches.last).to_i + 1, "import consumer from '../channels/consumer'\n"
31 | end
32 |
33 | initialize_line = lines.find { |line| line.start_with?("Futurism.initializeElements") }
34 | lines << "Futurism.initializeElements()\n" unless initialize_line
35 |
36 | subscribe_line = lines.find { |line| line.start_with?("Futurism.createSubscription") }
37 | lines << "Futurism.createSubscription(consumer)\n" unless subscribe_line
38 |
39 | File.write(filepath, lines.join)
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/test/dummy/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
9 | threads min_threads_count, max_threads_count
10 |
11 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
12 | #
13 | port ENV.fetch("PORT") { 3000 }
14 |
15 | # Specifies the `environment` that Puma will run in.
16 | #
17 | environment ENV.fetch("RAILS_ENV") { "development" }
18 |
19 | # Specifies the `pidfile` that Puma will use.
20 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
21 |
22 | # Specifies the number of `workers` to boot in clustered mode.
23 | # Workers are forked web server processes. If using threads and workers together
24 | # the concurrency of the application would be max `threads` * `workers`.
25 | # Workers do not work on JRuby or Windows (both of which do not support
26 | # processes).
27 | #
28 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
29 |
30 | # Use the `preload_app!` method when specifying a `workers` number.
31 | # This directive tells Puma to first boot the application and load code
32 | # before forking the application. This takes advantage of Copy On Write
33 | # process behavior so workers use less memory.
34 | #
35 | # preload_app!
36 |
37 | # Allow puma to be restarted by `rails restart` command.
38 | plugin :tmp_restart
39 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from '@rollup/plugin-node-resolve'
2 | import commonjs from '@rollup/plugin-commonjs'
3 | import json from '@rollup/plugin-json'
4 | import { terser } from 'rollup-plugin-terser'
5 |
6 | const pretty = () => {
7 | return terser({
8 | mangle: false,
9 | compress: false,
10 | format: {
11 | beautify: true,
12 | indent_level: 2
13 | }
14 | })
15 | }
16 |
17 | const minify = () => {
18 | return terser({
19 | mangle: true,
20 | compress: true
21 | })
22 | }
23 |
24 | const esConfig = {
25 | format: 'es',
26 | inlineDynamicImports: true
27 | }
28 |
29 | const umdConfig = {
30 | name: 'Futurism',
31 | format: 'umd',
32 | exports: 'named',
33 | globals: {
34 | cable_ready: 'CableReady'
35 | }
36 | }
37 |
38 | const distFolders = ['dist/', 'app/assets/javascripts/']
39 |
40 | const output = distFolders
41 | .map(distFolder => [
42 | {
43 | ...esConfig,
44 | file: `${distFolder}/futurism.js`,
45 | plugins: [pretty()]
46 | },
47 | {
48 | ...esConfig,
49 | file: `${distFolder}/futurism.min.js`,
50 | sourcemap: true,
51 | plugins: [minify()]
52 | },
53 | {
54 | ...umdConfig,
55 | file: `${distFolder}/futurism.umd.js`,
56 | plugins: [pretty()]
57 | },
58 | {
59 | ...umdConfig,
60 | file: `${distFolder}/futurism.umd.min.js`,
61 | sourcemap: true,
62 | plugins: [minify()]
63 | }
64 | ])
65 | .flat()
66 |
67 | export default [
68 | {
69 | external: ['cable_ready'],
70 | input: 'javascript/index.js',
71 | output,
72 | plugins: [commonjs(), resolve(), json()],
73 | watch: {
74 | include: 'javascript/**'
75 | }
76 | }
77 | ]
78 |
--------------------------------------------------------------------------------
/test/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | # The test environment is used exclusively to run your application's
2 | # test suite. You never need to work with it otherwise. Remember that
3 | # your test database is "scratch space" for the test suite and is wiped
4 | # and recreated between test runs. Don't rely on the data there!
5 |
6 | Rails.application.configure do
7 | # Settings specified here will take precedence over those in config/application.rb.
8 |
9 | config.cache_classes = false
10 | config.action_view.cache_template_loading = true
11 |
12 | # Do not eager load code on boot. This avoids loading your whole application
13 | # just for the purpose of running a single test. If you are using a tool that
14 | # preloads Rails for running tests, you may have to set it to true.
15 | config.eager_load = false
16 |
17 | # Configure public file server for tests with Cache-Control for performance.
18 | config.public_file_server.enabled = true
19 | config.public_file_server.headers = {
20 | "Cache-Control" => "public, max-age=#{1.hour.to_i}"
21 | }
22 |
23 | # Show full error reports and disable caching.
24 | config.consider_all_requests_local = true
25 | config.action_controller.perform_caching = false
26 | config.cache_store = :null_store
27 |
28 | # Raise exceptions instead of rendering exception templates.
29 | config.action_dispatch.show_exceptions = false
30 |
31 | # Disable request forgery protection in test environment.
32 | config.action_controller.allow_forgery_protection = false
33 |
34 | # Print deprecation notices to the stderr.
35 | config.active_support.deprecation = :stderr
36 |
37 | # Raises error for missing translations.
38 | # config.action_view.raise_on_missing_translations = true
39 | end
40 |
--------------------------------------------------------------------------------
/javascript/elements/futurism_utils.js:
--------------------------------------------------------------------------------
1 | /* global IntersectionObserver, CustomEvent, setTimeout */
2 |
3 | const dispatchAppearEvent = (entry, observer = null) => {
4 | if (!window.Futurism?.subscription) {
5 | return () => {
6 | setTimeout(() => dispatchAppearEvent(entry, observer)(), 1)
7 | }
8 | }
9 |
10 | const target = entry.target ? entry.target : entry
11 |
12 | const evt = new CustomEvent('futurism:appear', {
13 | bubbles: true,
14 | detail: {
15 | target,
16 | observer
17 | }
18 | })
19 |
20 | return () => {
21 | target.dispatchEvent(evt)
22 | }
23 | }
24 |
25 | // from https://advancedweb.hu/how-to-implement-an-exponential-backoff-retry-strategy-in-javascript/#rejection-based-retrying
26 | const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
27 |
28 | const callWithRetry = async (fn, depth = 0) => {
29 | try {
30 | return await fn()
31 | } catch (e) {
32 | if (depth > 10) {
33 | throw e
34 | }
35 | await wait(1.15 ** depth * 2000)
36 |
37 | return callWithRetry(fn, depth + 1)
38 | }
39 | }
40 |
41 | const observerCallback = (entries, observer) => {
42 | entries.forEach(async entry => {
43 | if (!entry.isIntersecting) return
44 |
45 | await callWithRetry(dispatchAppearEvent(entry, observer))
46 | })
47 | }
48 |
49 | export const extendElementWithIntersectionObserver = element => {
50 | Object.assign(element, {
51 | observer: new IntersectionObserver(observerCallback.bind(element), {})
52 | })
53 |
54 | if (!element.hasAttribute('keep')) {
55 | element.observer.observe(element)
56 | }
57 | }
58 |
59 | export const extendElementWithEagerLoading = element => {
60 | if (element.dataset.eager === 'true') {
61 | if (element.observer) element.observer.disconnect()
62 | callWithRetry(dispatchAppearEvent(element))
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/test/dummy/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 |
--------------------------------------------------------------------------------
/test/dummy/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 |
--------------------------------------------------------------------------------
/test/dummy/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 |
--------------------------------------------------------------------------------
/test/resolver/controller/instrumentation_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class DummyController < ActionController::Base
4 | def name_helper
5 | "FUTURISM".freeze
6 | end
7 | helper_method :name_helper
8 |
9 | def controller_and_action_helper
10 | [params["controller"], params["action"]].join(":")
11 | end
12 | helper_method :controller_and_action_helper
13 |
14 | def name_from_params_helper
15 | params["name"]
16 | end
17 | helper_method :name_from_params_helper
18 | end
19 |
20 | def dummy_connection
21 | connection = Minitest::Mock.new
22 | connection.expect(:env, {"HTTP_VAR" => "HTTP_VAR_VALUE"})
23 | connection
24 | end
25 |
26 | class Futurism::Resolver::Controller::InstrumentationTest < ActiveSupport::TestCase
27 | test "invokes ActiveSupport instrumentation on the Futurism render" do
28 | swap Futurism, instrumentation: true do
29 | events = []
30 | ActiveSupport::Notifications.subscribe("render.futurism") do |*args|
31 | events << ActiveSupport::Notifications::Event.new(*args)
32 | end
33 |
34 | renderer = Futurism::Resolver::Controller::Renderer.for(
35 | controller: DummyController,
36 | connection: dummy_connection,
37 | url: "posts/1",
38 | params: {channel: "Futurism::Channel"}
39 | )
40 | post = Post.create title: "Lorem"
41 | renderer.render(partial: "posts/card", locals: {post: post})
42 |
43 | assert_equal 1, events.size
44 | assert_equal "render.futurism", events.last.name
45 | assert_equal "Futurism::Channel", events.last.payload[:channel]
46 | assert_equal "posts", events.last.payload[:controller]
47 | assert_equal "show", events.last.payload[:action]
48 | assert_equal "posts/card", events.last.payload[:partial]
49 | end
50 | end
51 |
52 | test "does not invoke ActiveSupport instrumentation by default" do
53 | events = []
54 | ActiveSupport::Notifications.subscribe("render.futurism") do |*args|
55 | events << ActiveSupport::Notifications::Event.new(*args)
56 | end
57 |
58 | renderer = Futurism::Resolver::Controller::Renderer.for(
59 | controller: DummyController,
60 | connection: dummy_connection,
61 | url: "posts/1",
62 | params: {channel: "Futurism::Channel"}
63 | )
64 | post = Post.create title: "Lorem"
65 | renderer.render(partial: "posts/card", locals: {post: post})
66 |
67 | assert_empty events
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/lib/futurism/resolver/controller/renderer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Futurism
4 | module Resolver
5 | class Controller
6 | class Renderer
7 | HTTP_METHODS = [:get, :post, :put, :patch, :delete]
8 |
9 | def self.for(controller:, connection:, url:, params:)
10 | controller_renderer = new(
11 | controller: controller, connection: connection, url: url, params: params
12 | ).renderer
13 |
14 | Futurism.instrumentation? ? Instrumentation.new(controller_renderer) : controller_renderer
15 | end
16 |
17 | def initialize(controller:, connection:, url:, params:)
18 | @controller = controller
19 | @connection = connection
20 | @url = url || ""
21 | @params = params || {}
22 |
23 | setup_env!
24 | end
25 |
26 | def renderer
27 | @renderer ||= controller.renderer
28 | end
29 |
30 | private
31 |
32 | attr_reader :controller, :connection, :url, :params
33 | attr_writer :renderer
34 |
35 | def setup_env!
36 | unless url.nil?
37 | uri = URI.parse(url)
38 | path = ActionDispatch::Journey::Router::Utils.normalize_path(uri.path)
39 | query_hash = Rack::Utils.parse_nested_query(uri.query)
40 |
41 | path_params = recognize_url(url) # use full url to be more likely to match a url with subdomain constraints
42 |
43 | self.renderer =
44 | renderer.new(
45 | "rack.request.query_hash" => query_hash,
46 | "rack.request.query_string" => uri.query,
47 | "ORIGINAL_SCRIPT_NAME" => "",
48 | "ORIGINAL_FULLPATH" => path,
49 | Rack::SCRIPT_NAME => "",
50 | Rack::PATH_INFO => path,
51 | Rack::REQUEST_PATH => path,
52 | Rack::QUERY_STRING => uri.query,
53 | ActionDispatch::Http::Parameters::PARAMETERS_KEY => params.symbolize_keys.merge(path_params).merge(query_hash)
54 | )
55 | end
56 |
57 | # Copy connection env to renderer to fix some RACK related issues from gems like
58 | # Warden or Devise
59 | new_env = connection.env.merge(renderer.instance_variable_get(:@env))
60 | renderer.instance_variable_set(:@env, new_env)
61 | end
62 |
63 | def recognize_url(url)
64 | HTTP_METHODS.each do |http_method|
65 | path = Rails.application.routes.recognize_path(url, method: http_method)
66 | return path if path
67 | rescue ActionController::RoutingError
68 | # Route not matched, try next
69 | end
70 |
71 | warn "We were unable to find a matching rails route for '#{url}'. " \
72 | "This may be because there are proc-based routing constraints for this particular url, or " \
73 | "it truly is an unrecognizable url."
74 |
75 | {}
76 | end
77 | end
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/lib/futurism/resolver/resources.rb:
--------------------------------------------------------------------------------
1 | module Futurism
2 | module Resolver
3 | class Resources
4 | include Futurism::MessageVerifier
5 | include Futurism::OptionsTransformer
6 |
7 | # resource definitions are an array of [signed_params, sgid, signed_controller, url, broadcast_each]
8 | def initialize(resource_definitions:, connection:, params:)
9 | @connection = connection
10 | @params = params
11 | @resources_with_sgids, @resources_without_sgids = resource_definitions
12 | .partition { |signed_params, sgid, *| sgid.present? }
13 | .map { |partition| partition.map { |definition| ResourceDefinition.new(definition) } }
14 | end
15 |
16 | def resolve
17 | resolved_models.zip(@resources_with_sgids).each do |model, resource_definition|
18 | html = renderer_for(resource_definition: resource_definition).render(model)
19 |
20 | yield(resource_definition.selector, html, resource_definition.broadcast_each)
21 | end
22 |
23 | @resources_without_sgids.each do |resource_definition|
24 | options = options_from_resource(resource_definition)
25 | renderer = renderer_for(resource_definition: resource_definition)
26 | html =
27 | begin
28 | renderer.render(options)
29 | rescue => exception
30 | error_renderer.render(exception)
31 | end
32 |
33 | yield(resource_definition.selector, html, resource_definition.broadcast_each)
34 | end
35 | end
36 |
37 | private
38 |
39 | def error_renderer
40 | ErrorRenderer.new
41 | end
42 |
43 | class ResourceDefinition
44 | attr_reader :signed_params, :sgid, :signed_controller, :url
45 |
46 | def initialize(resource_definition)
47 | @signed_params, @sgid, @signed_controller, @url, @broadcast_each = resource_definition
48 | end
49 |
50 | def selector
51 | selector = "[data-signed-params='#{@signed_params}']"
52 | selector << "[data-sgid='#{@sgid}']" if @sgid.present?
53 | selector
54 | end
55 |
56 | def controller
57 | Resolver::Controller.from(signed_string: @signed_controller)
58 | end
59 |
60 | def broadcast_each
61 | @broadcast_each == "true"
62 | end
63 | end
64 |
65 | class ErrorRenderer
66 | include ActionView::Helpers::TagHelper
67 |
68 | def render(exception)
69 | return "" unless render?
70 |
71 | Futurism.logger.error(exception.to_s)
72 | Futurism.logger.error(exception.backtrace)
73 |
74 | tag.div { tag.span(exception.to_s) + tag.div(exception.backtrace.join("\n"), style: "display: none;") }
75 | end
76 |
77 | def render?
78 | Rails.env.development? || Rails.env.test?
79 | end
80 |
81 | attr_accessor :output_buffer
82 | end
83 |
84 | def renderer_for(resource_definition:)
85 | Resolver::Controller::Renderer.for(controller: resource_definition.controller,
86 | connection: @connection,
87 | url: resource_definition.url,
88 | params: @params)
89 | end
90 |
91 | def resolved_models
92 | GlobalID::Locator.locate_many_signed @resources_with_sgids.map(&:sgid)
93 | end
94 |
95 | def options_from_resource(resource_definition)
96 | load_options(message_verifier
97 | .verify(resource_definition.signed_params))
98 | end
99 | end
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/javascript/elements/index.js:
--------------------------------------------------------------------------------
1 | /* global customElements, sessionStorage */
2 |
3 | import FuturismElement from './futurism_element'
4 | import FuturismTableRow from './futurism_table_row'
5 | import FuturismLI from './futurism_li'
6 |
7 | import { sha256 } from '../utils/crypto'
8 |
9 | const polyfillCustomElements = () => {
10 | const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
11 |
12 | if (customElements) {
13 | if (isSafari) {
14 | document.write(
15 | '