proscenium 0.1.0.alpha1-x86_64-darwin
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/README.md +91 -0
- data/lib/proscenium/cli/argument_error.js +24 -0
- data/lib/proscenium/cli/builders/index.js +1 -0
- data/lib/proscenium/cli/builders/javascript.js +45 -0
- data/lib/proscenium/cli/builders/react.js +60 -0
- data/lib/proscenium/cli/builders/solid.js +46 -0
- data/lib/proscenium/cli/esbuild/env_plugin.js +21 -0
- data/lib/proscenium/cli/esbuild/resolve_plugin.js +136 -0
- data/lib/proscenium/cli/esbuild/solidjs_plugin.js +23 -0
- data/lib/proscenium/cli/js_builder.js +194 -0
- data/lib/proscenium/cli/solid.js +15 -0
- data/lib/proscenium/cli/utils.js +93 -0
- data/lib/proscenium/compiler.js +84 -0
- data/lib/proscenium/compilers/esbuild/argument_error.js +22 -0
- data/lib/proscenium/compilers/esbuild/env_plugin.js +21 -0
- data/lib/proscenium/compilers/esbuild/resolve_plugin.js +145 -0
- data/lib/proscenium/compilers/esbuild/setup_plugin.js +35 -0
- data/lib/proscenium/compilers/esbuild.bench.js +9 -0
- data/lib/proscenium/compilers/esbuild.js +82 -0
- data/lib/proscenium/css_module.rb +22 -0
- data/lib/proscenium/current.rb +9 -0
- data/lib/proscenium/helper.rb +32 -0
- data/lib/proscenium/link_to_helper.rb +49 -0
- data/lib/proscenium/middleware/base.rb +94 -0
- data/lib/proscenium/middleware/esbuild.rb +27 -0
- data/lib/proscenium/middleware/parcel_css.rb +37 -0
- data/lib/proscenium/middleware/runtime.rb +22 -0
- data/lib/proscenium/middleware/static.rb +14 -0
- data/lib/proscenium/middleware.rb +66 -0
- data/lib/proscenium/precompile.rb +31 -0
- data/lib/proscenium/railtie.rb +116 -0
- data/lib/proscenium/runtime/auto_reload.js +22 -0
- data/lib/proscenium/runtime/component_manager/index.js +27 -0
- data/lib/proscenium/runtime/component_manager/render_component.js +40 -0
- data/lib/proscenium/runtime/import_css.js +46 -0
- data/lib/proscenium/runtime/react_shim/index.js +1 -0
- data/lib/proscenium/runtime/react_shim/package.json +5 -0
- data/lib/proscenium/side_load.rb +96 -0
- data/lib/proscenium/version.rb +5 -0
- data/lib/proscenium/view_component/tag_builder.rb +23 -0
- data/lib/proscenium/view_component.rb +38 -0
- data/lib/proscenium.rb +18 -0
- data/lib/tasks/assets.rake +19 -0
- metadata +179 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Proscenium::CssModule
|
4
|
+
def initialize(path)
|
5
|
+
@path = "#{path}.module.css"
|
6
|
+
|
7
|
+
return unless Rails.application.config.proscenium.side_load
|
8
|
+
|
9
|
+
Proscenium::SideLoad.append! Rails.root.join(@path)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns an Array of class names generated from the given CSS module `names`.
|
13
|
+
def class_names(*names)
|
14
|
+
names.flatten.compact.map { |name| "#{name}#{hash}" }
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def hash
|
20
|
+
@hash ||= Digest::SHA1.hexdigest("/#{@path}")[..7]
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
module Helper
|
5
|
+
def compute_asset_path(path, options = {})
|
6
|
+
return "/#{path}" if %i[javascript stylesheet].include?(options[:type])
|
7
|
+
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
def side_load_stylesheets
|
12
|
+
return unless Proscenium::Current.loaded
|
13
|
+
|
14
|
+
stylesheet_link_tag(*Proscenium::Current.loaded[:css])
|
15
|
+
end
|
16
|
+
|
17
|
+
def side_load_javascripts(**options)
|
18
|
+
return unless Proscenium::Current.loaded
|
19
|
+
|
20
|
+
javascript_include_tag(*Proscenium::Current.loaded[:js], options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def proscenium_dev
|
24
|
+
return if !Rails.env.development? || !Proscenium::Railtie.websocket
|
25
|
+
|
26
|
+
javascript_tag %(
|
27
|
+
import autoReload from '/proscenium-runtime/auto_reload.js';
|
28
|
+
autoReload('#{Proscenium::Railtie.websocket_mount_path}');
|
29
|
+
), type: 'module'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
module LinkToHelper
|
5
|
+
# Overrides ActionView::Helpers::UrlHelper#link_to to allow passing a component instance as the
|
6
|
+
# URL, which will build the URL from the component path, eg. `/components/my_component`. The
|
7
|
+
# resulting link tag will also populate the `data` attribute with the component props.
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
# link_to 'Go to', MyComponent
|
11
|
+
#
|
12
|
+
# TODO: ummm, todo it! ;)
|
13
|
+
def link_to(*args, &block)
|
14
|
+
# name_argument_index = block ? 0 : 1
|
15
|
+
# if (args[name_argument_index]).respond_to?(:render_in)
|
16
|
+
# return super(*LinkToComponentArguments.new(args, name_argument_index,
|
17
|
+
# self).helper_options, &block)
|
18
|
+
# end
|
19
|
+
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Component handling for the `link_to` helper.
|
25
|
+
class LinkToComponentArguments
|
26
|
+
def initialize(options, name_argument_index, context)
|
27
|
+
@options = options
|
28
|
+
@name_argument_index = name_argument_index
|
29
|
+
@component = @options[@name_argument_index]
|
30
|
+
|
31
|
+
# We have to render the component, and then extract the props from the component. Rendering
|
32
|
+
# first ensures that we have all the correct props.
|
33
|
+
context.render @component
|
34
|
+
end
|
35
|
+
|
36
|
+
def helper_options
|
37
|
+
@options[@name_argument_index] = "/components#{@component.virtual_path}"
|
38
|
+
@options[@name_argument_index += 1] ||= {}
|
39
|
+
@options[@name_argument_index][:rel] = 'nofollow'
|
40
|
+
@options[@name_argument_index][:data] ||= {}
|
41
|
+
@options[@name_argument_index][:data][:component] = {
|
42
|
+
path: @component.virtual_path,
|
43
|
+
props: @component.props
|
44
|
+
}
|
45
|
+
|
46
|
+
@options
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
module Proscenium
|
6
|
+
class Middleware
|
7
|
+
class Base
|
8
|
+
include ActiveSupport::Benchmarkable
|
9
|
+
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
def self.attempt(request)
|
13
|
+
new(request).renderable!&.attempt
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(request)
|
17
|
+
@request = request
|
18
|
+
end
|
19
|
+
|
20
|
+
def renderable!
|
21
|
+
renderable? ? self : nil
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def renderable?
|
27
|
+
file_readable?
|
28
|
+
end
|
29
|
+
|
30
|
+
def file_readable?(file = @request.path_info)
|
31
|
+
return unless (path = clean_path(file))
|
32
|
+
|
33
|
+
file_stat = File.stat(Pathname(root).join(path.delete_prefix('/').b).to_s)
|
34
|
+
rescue SystemCallError
|
35
|
+
false
|
36
|
+
else
|
37
|
+
file_stat.file? && file_stat.readable?
|
38
|
+
end
|
39
|
+
|
40
|
+
def clean_path(file)
|
41
|
+
path = Rack::Utils.unescape_path file.chomp('/').delete_prefix('/')
|
42
|
+
Rack::Utils.clean_path_info path if Rack::Utils.valid_path? path
|
43
|
+
end
|
44
|
+
|
45
|
+
def root
|
46
|
+
@root ||= Rails.root.to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
def content_type
|
50
|
+
@content_type ||
|
51
|
+
::Rack::Mime.mime_type(::File.extname(@request.path_info), nil) ||
|
52
|
+
'application/javascript'
|
53
|
+
end
|
54
|
+
|
55
|
+
def render_response(content)
|
56
|
+
response = Rack::Response.new
|
57
|
+
response.write content
|
58
|
+
response.content_type = content_type
|
59
|
+
response['X-Proscenium-Middleware'] = name
|
60
|
+
response.finish
|
61
|
+
end
|
62
|
+
|
63
|
+
def build(cmd)
|
64
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
65
|
+
|
66
|
+
raise Error, stderr unless status.success?
|
67
|
+
unless stderr.empty?
|
68
|
+
raise "Proscenium build of #{name}:'#{@request.fullpath}' failed -- #{stderr}"
|
69
|
+
end
|
70
|
+
|
71
|
+
stdout
|
72
|
+
end
|
73
|
+
|
74
|
+
def benchmark(type)
|
75
|
+
super logging_message(type)
|
76
|
+
end
|
77
|
+
|
78
|
+
# rubocop:disable Style/FormatStringToken
|
79
|
+
def logging_message(type)
|
80
|
+
format '[Proscenium] Request (%s) %s for %s at %s',
|
81
|
+
type, @request.fullpath, @request.ip, Time.now.to_default_s
|
82
|
+
end
|
83
|
+
# rubocop:enable Style/FormatStringToken
|
84
|
+
|
85
|
+
def logger
|
86
|
+
Rails.logger
|
87
|
+
end
|
88
|
+
|
89
|
+
def name
|
90
|
+
@name ||= self.class.name.split('::').last.downcase
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
class Middleware
|
5
|
+
class Esbuild < Base
|
6
|
+
def attempt
|
7
|
+
benchmark :esbuild do
|
8
|
+
render_response build("#{cli} --root #{root} #{path}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def path
|
15
|
+
@request.path[1..]
|
16
|
+
end
|
17
|
+
|
18
|
+
def cli
|
19
|
+
if ENV['PROSCENIUM_TEST']
|
20
|
+
'deno run -q --import-map import_map.json -A lib/proscenium/compilers/esbuild.js'
|
21
|
+
else
|
22
|
+
Gem.bin_path 'proscenium', 'esbuild'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'oj'
|
4
|
+
|
5
|
+
module Proscenium
|
6
|
+
class Middleware
|
7
|
+
class ParcelCss < Base
|
8
|
+
def attempt
|
9
|
+
benchmark :parcelcss do
|
10
|
+
results = build("#{cli} #{cli_options.join ' '} #{root}#{@request.path}")
|
11
|
+
render_response css_module? ? Oj.load(results, mode: :strict)['code'] : results
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def cli
|
18
|
+
Gem.bin_path 'proscenium', 'parcel_css'
|
19
|
+
end
|
20
|
+
|
21
|
+
def cli_options
|
22
|
+
options = ['--nesting', '--custom-media', '--targets', "'>= 0.25%'"]
|
23
|
+
|
24
|
+
if css_module?
|
25
|
+
hash = Digest::SHA1.hexdigest(@request.path)[..7]
|
26
|
+
options += ['--css-modules', '--css-modules-pattern', "'[local]#{hash}'"]
|
27
|
+
end
|
28
|
+
|
29
|
+
Rails.env.production? ? options << '-m' : options
|
30
|
+
end
|
31
|
+
|
32
|
+
def css_module?
|
33
|
+
@css_module ||= /\.module\.css$/i.match?(@request.path_info)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
class Middleware
|
5
|
+
class Runtime < Esbuild
|
6
|
+
private
|
7
|
+
|
8
|
+
def renderable?
|
9
|
+
old_root = root
|
10
|
+
old_path_info = @request.path_info
|
11
|
+
|
12
|
+
@root = Pathname.new(__dir__).join('../')
|
13
|
+
@request.path_info = @request.path_info.sub(%r{^/proscenium-runtime/}, 'runtime/')
|
14
|
+
|
15
|
+
super
|
16
|
+
ensure
|
17
|
+
@request.path_info = old_path_info
|
18
|
+
@root = old_root
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
module Middleware
|
5
|
+
# Serves static files from disk that end with .js or .css.
|
6
|
+
class Static < Base
|
7
|
+
def attempt
|
8
|
+
benchmark :static do
|
9
|
+
Rack::File.new(root, { 'X-Proscenium-Middleware' => 'static' }).call(@request.env)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
class Middleware
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
autoload :Base
|
8
|
+
autoload :Esbuild
|
9
|
+
autoload :ParcelCss
|
10
|
+
autoload :Runtime
|
11
|
+
|
12
|
+
MIDDLEWARE_CLASSES = {
|
13
|
+
esbuild: Esbuild,
|
14
|
+
parcelcss: ParcelCss,
|
15
|
+
runtime: Runtime
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
def initialize(app)
|
19
|
+
@app = app
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(env)
|
23
|
+
request = Rack::Request.new(env)
|
24
|
+
|
25
|
+
return @app.call(env) if !request.get? && !request.head?
|
26
|
+
|
27
|
+
attempt(request) || @app.call(env)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Look for the precompiled file in public/assets first, then fallback to the Proscenium
|
33
|
+
# middleware that matches the type of file requested, ie: .js => esbuild.
|
34
|
+
# See Rails.application.config.proscenium.glob_types.
|
35
|
+
def attempt(request)
|
36
|
+
return unless (type = find_type(request))
|
37
|
+
|
38
|
+
file_handler.attempt(request.env) || MIDDLEWARE_CLASSES[type].attempt(request)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the type of file being requested using Rails.application.config.proscenium.glob_types.
|
42
|
+
def find_type(request)
|
43
|
+
return :runtime if request.path_info.start_with?('/proscenium-runtime/')
|
44
|
+
|
45
|
+
path = Rails.root.join(request.path[1..])
|
46
|
+
|
47
|
+
type, = glob_types.find do |_, globs|
|
48
|
+
# TODO: Look for the precompiled file in public/assets first
|
49
|
+
# globs.any? { |glob| Rails.public_path.join('assets').glob(glob).any?(path) }
|
50
|
+
|
51
|
+
globs.any? { |glob| Rails.root.glob(glob).any?(path) }
|
52
|
+
end
|
53
|
+
|
54
|
+
type
|
55
|
+
end
|
56
|
+
|
57
|
+
def file_handler
|
58
|
+
::ActionDispatch::FileHandler.new Rails.public_path.join('assets').to_s,
|
59
|
+
headers: { 'X-Proscenium-Middleware' => 'precompiled' }
|
60
|
+
end
|
61
|
+
|
62
|
+
def glob_types
|
63
|
+
Rails.application.config.proscenium.glob_types
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
module Proscenium
|
6
|
+
class Precompile
|
7
|
+
def self.call
|
8
|
+
new.call
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
Rails.application.config.proscenium.glob_types.find do |type, globs|
|
13
|
+
cmd = "#{cli type} --root #{Rails.root} '#{globs.join "' '"}' --write"
|
14
|
+
_, stderr, status = Open3.capture3(cmd)
|
15
|
+
|
16
|
+
raise stderr unless status.success?
|
17
|
+
raise "#{type} compiliation failed -- #{stderr}" unless stderr.empty?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def cli(type)
|
24
|
+
if ENV['PROSCENIUM_TEST']
|
25
|
+
"deno run -q --import-map import_map.json -A lib/proscenium/compilers/#{type}.js"
|
26
|
+
else
|
27
|
+
Gem.bin_path 'proscenium', type.to_s
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails'
|
4
|
+
require 'action_cable/engine'
|
5
|
+
require 'listen'
|
6
|
+
|
7
|
+
ENV['RAILS_ENV'] = Rails.env
|
8
|
+
|
9
|
+
module Proscenium
|
10
|
+
# These globs should actually be Deno supported globs, and not ruby globs. This is because when
|
11
|
+
# precompiling, the glob paths are passed as is to the compiler run by Deno.
|
12
|
+
#
|
13
|
+
# See https://doc.deno.land/https://deno.land/std@0.145.0/path/mod.ts/~/globToRegExp
|
14
|
+
DEFAULT_GLOB_TYPES = {
|
15
|
+
esbuild: [
|
16
|
+
'lib/**/*.{js,jsx}',
|
17
|
+
'app/components/**/*.{js,jsx}',
|
18
|
+
'app/views/**/*.{js,jsx}'
|
19
|
+
],
|
20
|
+
parcelcss: [
|
21
|
+
'lib/**/*.css',
|
22
|
+
'app/components/**/*.css',
|
23
|
+
'app/views/**/*.css'
|
24
|
+
]
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def config
|
29
|
+
@config ||= Railtie.config.proscenium
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Railtie < ::Rails::Engine
|
34
|
+
isolate_namespace Proscenium
|
35
|
+
|
36
|
+
config.proscenium = ActiveSupport::OrderedOptions.new
|
37
|
+
config.proscenium.listen_paths ||= %w[lib app]
|
38
|
+
config.proscenium.listen_extensions ||= /\.(css|jsx?)$/
|
39
|
+
config.proscenium.side_load = true
|
40
|
+
|
41
|
+
initializer 'proscenium.configuration' do |app|
|
42
|
+
options = app.config.proscenium
|
43
|
+
|
44
|
+
options.glob_types = DEFAULT_GLOB_TYPES if options.glob_types.blank?
|
45
|
+
|
46
|
+
options.auto_refresh = true if options.auto_refresh.nil?
|
47
|
+
options.listen = Rails.env.development? if options.listen.nil?
|
48
|
+
options.listen_paths.filter! { |path| Dir.exist? path }
|
49
|
+
options.cable_mount_path ||= '/proscenium-cable'
|
50
|
+
options.cable_logger ||= Rails.logger
|
51
|
+
end
|
52
|
+
|
53
|
+
initializer 'proscenium.side_load' do |_app|
|
54
|
+
Proscenium::Current.loaded ||= SideLoad::EXTENSIONS.to_h { |e| [e, Set[]] }
|
55
|
+
end
|
56
|
+
|
57
|
+
initializer 'proscenium.middleware' do |app|
|
58
|
+
app.middleware.insert_after ActionDispatch::Static, Proscenium::Middleware
|
59
|
+
app.middleware.insert_after ActionDispatch::Static, Rack::ETag, 'no-cache'
|
60
|
+
app.middleware.insert_after ActionDispatch::Static, Rack::ConditionalGet
|
61
|
+
end
|
62
|
+
|
63
|
+
initializer 'proscenium.helpers' do |_app|
|
64
|
+
ActiveSupport.on_load(:action_view) do
|
65
|
+
ActionView::Base.include Proscenium::Helper
|
66
|
+
|
67
|
+
if Rails.application.config.proscenium.side_load
|
68
|
+
ActionView::TemplateRenderer.prepend SideLoad::Monkey::TemplateRenderer
|
69
|
+
end
|
70
|
+
|
71
|
+
ActionView::Helpers::UrlHelper.prepend Proscenium::LinkToHelper
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
config.after_initialize do
|
76
|
+
next unless config.proscenium.listen
|
77
|
+
|
78
|
+
@listener = Listen.to(*config.proscenium.listen_paths,
|
79
|
+
only: config.proscenium.listen_extensions) do |mod, add, rem|
|
80
|
+
Proscenium::Railtie.websocket&.broadcast('reload', {
|
81
|
+
modified: mod,
|
82
|
+
removed: rem,
|
83
|
+
added: add
|
84
|
+
})
|
85
|
+
end
|
86
|
+
|
87
|
+
@listener.start
|
88
|
+
end
|
89
|
+
|
90
|
+
at_exit do
|
91
|
+
@listener&.stop
|
92
|
+
end
|
93
|
+
|
94
|
+
class << self
|
95
|
+
def websocket
|
96
|
+
return unless config.proscenium.auto_refresh
|
97
|
+
|
98
|
+
cable = ActionCable::Server::Configuration.new
|
99
|
+
cable.cable = { adapter: 'async' }.with_indifferent_access
|
100
|
+
cable.mount_path = config.proscenium.cable_mount_path
|
101
|
+
cable.connection_class = -> { Proscenium::Connection }
|
102
|
+
cable.logger = config.proscenium.cable_logger
|
103
|
+
|
104
|
+
@websocket ||= ActionCable::Server::Base.new(config: cable)
|
105
|
+
end
|
106
|
+
|
107
|
+
def websocket_mount_path
|
108
|
+
"#{mounted_path}#{config.proscenium.cable_mount_path}" if websocket
|
109
|
+
end
|
110
|
+
|
111
|
+
def mounted_path
|
112
|
+
Proscenium::Railtie.routes.find_script_name({})
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { createConsumer } from 'https://esm.sh/@rails/actioncable@6.0.5'
|
2
|
+
import debounce from 'https://esm.sh/debounce@1.2.1'
|
3
|
+
|
4
|
+
export default socketPath => {
|
5
|
+
const uid = (Date.now() + ((Math.random() * 100) | 0)).toString()
|
6
|
+
const consumer = createConsumer(`${socketPath}?uid=${uid}`)
|
7
|
+
|
8
|
+
consumer.subscriptions.create('Proscenium::ReloadChannel', {
|
9
|
+
received: debounce(() => {
|
10
|
+
console.log('Proscenium files changed; reloading...')
|
11
|
+
location.reload()
|
12
|
+
}, 200),
|
13
|
+
|
14
|
+
connected() {
|
15
|
+
console.log('Proscenium auto reload websocket connected')
|
16
|
+
},
|
17
|
+
|
18
|
+
disconnected() {
|
19
|
+
console.log('Proscenium auto reload websocket disconnected')
|
20
|
+
}
|
21
|
+
})
|
22
|
+
}
|
@@ -0,0 +1,27 @@
|
|
1
|
+
/* eslint-disable no-console */
|
2
|
+
|
3
|
+
import renderComponent from `/proscenium-runtime/component_manager/render_component.js`
|
4
|
+
|
5
|
+
document.addEventListener('DOMContentLoaded', () => {
|
6
|
+
const elements = document.querySelectorAll('[data-component]')
|
7
|
+
|
8
|
+
if (elements.length < 1) return
|
9
|
+
|
10
|
+
Array.from(elements, (ele) => {
|
11
|
+
const data = JSON.parse(ele.getAttribute('data-component'))
|
12
|
+
|
13
|
+
let isVisible = false
|
14
|
+
const observer = new IntersectionObserver((entries) => {
|
15
|
+
entries.forEach((entry) => {
|
16
|
+
if (!isVisible && entry.isIntersecting) {
|
17
|
+
isVisible = true
|
18
|
+
observer.unobserve(ele)
|
19
|
+
|
20
|
+
renderComponent(ele, data)
|
21
|
+
}
|
22
|
+
})
|
23
|
+
})
|
24
|
+
|
25
|
+
observer.observe(ele)
|
26
|
+
})
|
27
|
+
})
|
@@ -0,0 +1,40 @@
|
|
1
|
+
/* eslint-disable no-console */
|
2
|
+
|
3
|
+
import { RAILS_ENV } from 'env'
|
4
|
+
|
5
|
+
// We don't use JSX, as doing so would auto-inject React. We don't want to do this, as React is lazy
|
6
|
+
// loaded only when needed.
|
7
|
+
export default async function (ele, data) {
|
8
|
+
const { createElement, useEffect, lazy, Suspense } = await import('react')
|
9
|
+
const { createRoot } = await import('react-dom/client')
|
10
|
+
|
11
|
+
const component = lazy(() => import(`/app/components${data.path}.jsx`))
|
12
|
+
const contentLoader = data.contentLoader && ele.firstElementChild
|
13
|
+
|
14
|
+
const Fallback = ({ contentLoader }) => {
|
15
|
+
useEffect(() => {
|
16
|
+
contentLoader && contentLoader.remove()
|
17
|
+
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
18
|
+
|
19
|
+
if (!contentLoader) return null
|
20
|
+
|
21
|
+
return /* @__PURE__ */ createElement('div', {
|
22
|
+
style: { height: '100%' },
|
23
|
+
dangerouslySetInnerHTML: { __html: contentLoader.outerHTML }
|
24
|
+
})
|
25
|
+
}
|
26
|
+
|
27
|
+
createRoot(ele).render(
|
28
|
+
/* @__PURE__ */ createElement(
|
29
|
+
Suspense,
|
30
|
+
{
|
31
|
+
fallback: /* @__PURE__ */ createElement(Fallback, {
|
32
|
+
contentLoader
|
33
|
+
})
|
34
|
+
},
|
35
|
+
createElement(component, data.props)
|
36
|
+
)
|
37
|
+
)
|
38
|
+
|
39
|
+
RAILS_ENV === 'development' && console.debug(`[REACT]`, `Rendered ${data.path.slice(1)}`)
|
40
|
+
}
|
@@ -0,0 +1,46 @@
|
|
1
|
+
async function digest(value) {
|
2
|
+
value = new TextEncoder().encode(value)
|
3
|
+
const view = new DataView(await crypto.subtle.digest('SHA-1', value))
|
4
|
+
|
5
|
+
let hexCodes = ''
|
6
|
+
for (let index = 0; index < view.byteLength; index += 4) {
|
7
|
+
hexCodes += view.getUint32(index).toString(16).padStart(8, '0')
|
8
|
+
}
|
9
|
+
|
10
|
+
return hexCodes.slice(0, 8)
|
11
|
+
}
|
12
|
+
|
13
|
+
const proxyCache = {}
|
14
|
+
|
15
|
+
export async function importCssModule(path) {
|
16
|
+
appendStylesheet(path)
|
17
|
+
|
18
|
+
if (Object.keys(proxyCache).includes(path)) {
|
19
|
+
return proxyCache[path]
|
20
|
+
}
|
21
|
+
|
22
|
+
const hashValue = await digest(path)
|
23
|
+
return (proxyCache[path] = new Proxy(
|
24
|
+
{},
|
25
|
+
{
|
26
|
+
get(target, prop, receiver) {
|
27
|
+
if (prop in target || typeof prop === 'symbol') {
|
28
|
+
return Reflect.get(target, prop, receiver)
|
29
|
+
} else {
|
30
|
+
return `${prop}${hashValue}`
|
31
|
+
}
|
32
|
+
}
|
33
|
+
}
|
34
|
+
))
|
35
|
+
}
|
36
|
+
|
37
|
+
export function appendStylesheet(path) {
|
38
|
+
// Make sure we only load the stylesheet once.
|
39
|
+
if (document.head.querySelector(`link[rel=stylesheet][href='${path}']`)) return
|
40
|
+
|
41
|
+
const ele = document.createElement('link')
|
42
|
+
ele.setAttribute('rel', 'stylesheet')
|
43
|
+
ele.setAttribute('media', 'all')
|
44
|
+
ele.setAttribute('href', path)
|
45
|
+
document.head.appendChild(ele)
|
46
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export { createElement as reactCreateElement, Fragment as ReactFragment } from 'react'
|