proscenium 0.1.0.alpha1-arm64-darwin
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|