├── 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 | 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 | 9 | 10 | 11 | 12 | 13 | 14 | <% @posts.each do |post| %> 15 | 16 | 17 | 18 | 19 | 20 | 21 | <% end %> 22 | 23 |
Title
<%= post.title %><%= link_to 'Show', post %><%= link_to 'Edit', edit_post_path(post) %><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %>
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 | '