proscenium 0.7.0-aarch64-linux
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/LICENSE.txt +21 -0
- data/README.md +336 -0
- data/lib/proscenium/css_module/class_names_resolver.rb +66 -0
- data/lib/proscenium/css_module/resolver.rb +76 -0
- data/lib/proscenium/css_module.rb +43 -0
- data/lib/proscenium/current.rb +9 -0
- data/lib/proscenium/esbuild/golib.rb +97 -0
- data/lib/proscenium/esbuild.rb +32 -0
- data/lib/proscenium/ext/proscenium +0 -0
- data/lib/proscenium/ext/proscenium.h +109 -0
- data/lib/proscenium/helper.rb +19 -0
- data/lib/proscenium/log_subscriber.rb +26 -0
- data/lib/proscenium/middleware/base.rb +100 -0
- data/lib/proscenium/middleware/esbuild.rb +32 -0
- data/lib/proscenium/middleware/url.rb +16 -0
- data/lib/proscenium/middleware.rb +59 -0
- data/lib/proscenium/phlex/component_concerns.rb +27 -0
- data/lib/proscenium/phlex/page.rb +62 -0
- data/lib/proscenium/phlex/react_component.rb +69 -0
- data/lib/proscenium/phlex/resolve_css_modules.rb +67 -0
- data/lib/proscenium/phlex.rb +61 -0
- data/lib/proscenium/railtie.rb +85 -0
- data/lib/proscenium/side_load/ensure_loaded.rb +25 -0
- data/lib/proscenium/side_load/helper.rb +25 -0
- data/lib/proscenium/side_load/monkey.rb +48 -0
- data/lib/proscenium/side_load.rb +78 -0
- data/lib/proscenium/version.rb +5 -0
- data/lib/proscenium/view_component/react_component.rb +35 -0
- data/lib/proscenium/view_component/tag_builder.rb +23 -0
- data/lib/proscenium/view_component.rb +55 -0
- data/lib/proscenium.rb +96 -0
- metadata +189 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ffi'
|
4
|
+
require 'oj'
|
5
|
+
|
6
|
+
module Proscenium
|
7
|
+
class Esbuild::Golib
|
8
|
+
class Result < FFI::Struct
|
9
|
+
layout :success, :bool,
|
10
|
+
:response, :string
|
11
|
+
end
|
12
|
+
|
13
|
+
module Request
|
14
|
+
extend FFI::Library
|
15
|
+
ffi_lib Pathname.new(__dir__).join('../ext/proscenium').to_s
|
16
|
+
|
17
|
+
enum :environment, [:development, 1, :test, :production]
|
18
|
+
|
19
|
+
attach_function :build, [
|
20
|
+
:string, # path or entry point
|
21
|
+
:string, # root
|
22
|
+
:string, # base URL of the Rails app. eg. https://example.com
|
23
|
+
:environment, # Rails environment as a Symbol
|
24
|
+
:string, # path to import map, relative to root
|
25
|
+
:bool # debugging enabled?
|
26
|
+
], Result.by_value
|
27
|
+
|
28
|
+
attach_function :resolve, [
|
29
|
+
:string, # path or entry point
|
30
|
+
:string, # root
|
31
|
+
:environment, # Rails environment as a Symbol
|
32
|
+
:string # path to import map, relative to root
|
33
|
+
], Result.by_value
|
34
|
+
end
|
35
|
+
|
36
|
+
class BuildError < StandardError
|
37
|
+
attr_reader :error, :path
|
38
|
+
|
39
|
+
def initialize(path, error)
|
40
|
+
error = Oj.load(error, mode: :strict).deep_transform_keys(&:underscore)
|
41
|
+
|
42
|
+
super "Failed to build '#{path}' -- #{error['text']}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class ResolveError < StandardError
|
47
|
+
attr_reader :error_msg, :path
|
48
|
+
|
49
|
+
def initialize(path, error_msg)
|
50
|
+
super "Failed to resolve '#{path}' -- #{error_msg}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(root: nil, base_url: nil)
|
55
|
+
@root = root || Rails.root
|
56
|
+
@base_url = base_url
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.resolve(path)
|
60
|
+
new.resolve(path)
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.build(path)
|
64
|
+
new.build(path)
|
65
|
+
end
|
66
|
+
|
67
|
+
def build(path)
|
68
|
+
result = Request.build(path, @root.to_s, @base_url, Rails.env.to_sym, import_map,
|
69
|
+
Rails.env.development?)
|
70
|
+
raise BuildError.new(path, result[:response]) unless result[:success]
|
71
|
+
|
72
|
+
result[:response]
|
73
|
+
end
|
74
|
+
|
75
|
+
def resolve(path)
|
76
|
+
result = Request.resolve(path, @root.to_s, Rails.env.to_sym, import_map)
|
77
|
+
raise ResolveError.new(path, result[:response]) unless result[:success]
|
78
|
+
|
79
|
+
result[:response]
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def import_map
|
85
|
+
return unless (path = Rails.root&.join('config'))
|
86
|
+
|
87
|
+
if (json = path.join('import_map.json')).exist?
|
88
|
+
return json.relative_path_from(@root).to_s
|
89
|
+
end
|
90
|
+
if (js = path.join('import_map.js')).exist?
|
91
|
+
return js.relative_path_from(@root).to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
class Esbuild
|
5
|
+
class CompileError < StandardError; end
|
6
|
+
|
7
|
+
extend ActiveSupport::Autoload
|
8
|
+
|
9
|
+
autoload :Golib
|
10
|
+
|
11
|
+
def self.build(...)
|
12
|
+
new(...).build
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(path, root:, base_url:)
|
16
|
+
@path = path
|
17
|
+
@root = root
|
18
|
+
@base_url = base_url
|
19
|
+
end
|
20
|
+
|
21
|
+
def build
|
22
|
+
Proscenium::Esbuild::Golib.new(root: @root, base_url: @base_url).build(@path)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def cache_query_string
|
28
|
+
q = Proscenium.config.cache_query_string
|
29
|
+
q ? "--cache-query-string #{q}" : nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
Binary file
|
@@ -0,0 +1,109 @@
|
|
1
|
+
/* Code generated by cmd/cgo; DO NOT EDIT. */
|
2
|
+
|
3
|
+
/* package joelmoss/proscenium */
|
4
|
+
|
5
|
+
|
6
|
+
#line 1 "cgo-builtin-export-prolog"
|
7
|
+
|
8
|
+
#include <stddef.h>
|
9
|
+
|
10
|
+
#ifndef GO_CGO_EXPORT_PROLOGUE_H
|
11
|
+
#define GO_CGO_EXPORT_PROLOGUE_H
|
12
|
+
|
13
|
+
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
14
|
+
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
|
15
|
+
#endif
|
16
|
+
|
17
|
+
#endif
|
18
|
+
|
19
|
+
/* Start of preamble from import "C" comments. */
|
20
|
+
|
21
|
+
|
22
|
+
#line 3 "main.go"
|
23
|
+
|
24
|
+
struct Result {
|
25
|
+
int success;
|
26
|
+
char* response;
|
27
|
+
};
|
28
|
+
|
29
|
+
#line 1 "cgo-generated-wrapper"
|
30
|
+
|
31
|
+
|
32
|
+
/* End of preamble from import "C" comments. */
|
33
|
+
|
34
|
+
|
35
|
+
/* Start of boilerplate cgo prologue. */
|
36
|
+
#line 1 "cgo-gcc-export-header-prolog"
|
37
|
+
|
38
|
+
#ifndef GO_CGO_PROLOGUE_H
|
39
|
+
#define GO_CGO_PROLOGUE_H
|
40
|
+
|
41
|
+
typedef signed char GoInt8;
|
42
|
+
typedef unsigned char GoUint8;
|
43
|
+
typedef short GoInt16;
|
44
|
+
typedef unsigned short GoUint16;
|
45
|
+
typedef int GoInt32;
|
46
|
+
typedef unsigned int GoUint32;
|
47
|
+
typedef long long GoInt64;
|
48
|
+
typedef unsigned long long GoUint64;
|
49
|
+
typedef GoInt64 GoInt;
|
50
|
+
typedef GoUint64 GoUint;
|
51
|
+
typedef size_t GoUintptr;
|
52
|
+
typedef float GoFloat32;
|
53
|
+
typedef double GoFloat64;
|
54
|
+
#ifdef _MSC_VER
|
55
|
+
#include <complex.h>
|
56
|
+
typedef _Fcomplex GoComplex64;
|
57
|
+
typedef _Dcomplex GoComplex128;
|
58
|
+
#else
|
59
|
+
typedef float _Complex GoComplex64;
|
60
|
+
typedef double _Complex GoComplex128;
|
61
|
+
#endif
|
62
|
+
|
63
|
+
/*
|
64
|
+
static assertion to make sure the file is being used on architecture
|
65
|
+
at least with matching size of GoInt.
|
66
|
+
*/
|
67
|
+
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
|
68
|
+
|
69
|
+
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
70
|
+
typedef _GoString_ GoString;
|
71
|
+
#endif
|
72
|
+
typedef void *GoMap;
|
73
|
+
typedef void *GoChan;
|
74
|
+
typedef struct { void *t; void *v; } GoInterface;
|
75
|
+
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
|
76
|
+
|
77
|
+
#endif
|
78
|
+
|
79
|
+
/* End of boilerplate cgo prologue. */
|
80
|
+
|
81
|
+
#ifdef __cplusplus
|
82
|
+
extern "C" {
|
83
|
+
#endif
|
84
|
+
|
85
|
+
|
86
|
+
// Build the given `path` in the `root`.
|
87
|
+
//
|
88
|
+
// - path - The path to build relative to `root`.
|
89
|
+
// - root - The working directory.
|
90
|
+
// - baseUrl - base URL of the Rails app. eg. https://example.com
|
91
|
+
// - env - The environment (1 = development, 2 = test, 3 = production)
|
92
|
+
// - importMap - Path to the import map relative to `root`.
|
93
|
+
// - bundle
|
94
|
+
// - debug
|
95
|
+
//
|
96
|
+
extern struct Result build(char* filepath, char* root, char* baseUrl, unsigned int env, char* importMap, GoUint8 bundle, GoUint8 debug);
|
97
|
+
|
98
|
+
// Resolve the given `path` relative to the `root`.
|
99
|
+
//
|
100
|
+
// - path - The path to build relative to `root`.
|
101
|
+
// - root - The working directory.
|
102
|
+
// - env - The environment (1 = development, 2 = test, 3 = production)
|
103
|
+
// - importMap - Path to the import map relative to `root`.
|
104
|
+
//
|
105
|
+
extern struct Result resolve(char* path, char* root, unsigned int env, char* importMap);
|
106
|
+
|
107
|
+
#ifdef __cplusplus
|
108
|
+
}
|
109
|
+
#endif
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
module Helper
|
5
|
+
def compute_asset_path(path, options = {})
|
6
|
+
if %i[javascript stylesheet].include?(options[:type])
|
7
|
+
result = "/#{path}"
|
8
|
+
|
9
|
+
if (qs = Proscenium.config.cache_query_string)
|
10
|
+
result << "?#{qs}"
|
11
|
+
end
|
12
|
+
|
13
|
+
return result
|
14
|
+
end
|
15
|
+
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/log_subscriber'
|
4
|
+
|
5
|
+
module Proscenium
|
6
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
7
|
+
def sideload(event)
|
8
|
+
info do
|
9
|
+
" [Proscenium] Side loaded #{event.payload[:identifier]}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def build(event)
|
14
|
+
path = event.payload[:identifier]
|
15
|
+
path = path.start_with?(/https?%3A%2F%2F/) ? CGI.unescape(path) : path
|
16
|
+
|
17
|
+
info do
|
18
|
+
message = +"[Proscenium] Building #{path}"
|
19
|
+
message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
|
20
|
+
message << "\n" if defined?(Rails.env) && Rails.env.development?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Proscenium::LogSubscriber.attach_to :proscenium
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
require 'oj'
|
5
|
+
|
6
|
+
module Proscenium
|
7
|
+
class Middleware
|
8
|
+
class Base
|
9
|
+
include ActiveSupport::Benchmarkable
|
10
|
+
|
11
|
+
# Error when the result of the build returns an error. For example, when esbuild returns
|
12
|
+
# errors.
|
13
|
+
class CompileError < StandardError
|
14
|
+
attr_reader :detail, :file
|
15
|
+
|
16
|
+
def initialize(args)
|
17
|
+
@detail = args[:detail]
|
18
|
+
@file = args[:file]
|
19
|
+
super "Failed to build '#{args[:file]}' -- #{detail}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.attempt(request)
|
24
|
+
new(request).renderable!&.attempt
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(request)
|
28
|
+
@request = request
|
29
|
+
end
|
30
|
+
|
31
|
+
def renderable!
|
32
|
+
renderable? ? self : nil
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def real_path
|
38
|
+
@request.path
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [String] the path to the file without the leading slash which will be built.
|
42
|
+
def path_to_build
|
43
|
+
@request.path[1..]
|
44
|
+
end
|
45
|
+
|
46
|
+
def sourcemap?
|
47
|
+
@request.path.ends_with?('.map')
|
48
|
+
end
|
49
|
+
|
50
|
+
def renderable?
|
51
|
+
file_readable?
|
52
|
+
end
|
53
|
+
|
54
|
+
def file_readable?
|
55
|
+
return unless (path = clean_path(sourcemap? ? real_path[0...-4] : real_path))
|
56
|
+
|
57
|
+
file_stat = File.stat(Pathname(root).join(path.delete_prefix('/').b).to_s)
|
58
|
+
rescue SystemCallError
|
59
|
+
false
|
60
|
+
else
|
61
|
+
file_stat.file? && file_stat.readable?
|
62
|
+
end
|
63
|
+
|
64
|
+
def clean_path(file)
|
65
|
+
path = Rack::Utils.unescape_path file.chomp('/').delete_prefix('/')
|
66
|
+
Rack::Utils.clean_path_info path if Rack::Utils.valid_path? path
|
67
|
+
end
|
68
|
+
|
69
|
+
def root
|
70
|
+
@root ||= Rails.root.to_s
|
71
|
+
end
|
72
|
+
|
73
|
+
def content_type
|
74
|
+
@content_type ||
|
75
|
+
::Rack::Mime.mime_type(::File.extname(path_to_build), nil) ||
|
76
|
+
'application/javascript'
|
77
|
+
end
|
78
|
+
|
79
|
+
def render_response(content)
|
80
|
+
response = Rack::Response.new
|
81
|
+
response.write content
|
82
|
+
response.content_type = content_type
|
83
|
+
response['X-Proscenium-Middleware'] = name
|
84
|
+
response.set_header 'SourceMap', "#{@request.path_info}.map"
|
85
|
+
|
86
|
+
if Proscenium.config.cache_query_string && Proscenium.config.cache_max_age
|
87
|
+
response.cache! Proscenium.config.cache_max_age
|
88
|
+
end
|
89
|
+
|
90
|
+
yield response if block_given?
|
91
|
+
|
92
|
+
response.finish
|
93
|
+
end
|
94
|
+
|
95
|
+
def name
|
96
|
+
@name ||= self.class.name.split('::').last.downcase
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
class Middleware
|
5
|
+
class Esbuild < Base
|
6
|
+
class CompileError < Base::CompileError
|
7
|
+
def initialize(args)
|
8
|
+
detail = args[:detail]
|
9
|
+
detail = ActiveSupport::HashWithIndifferentAccess.new(Oj.load(detail, mode: :strict))
|
10
|
+
|
11
|
+
args[:detail] = if detail[:location]
|
12
|
+
"#{detail[:text]} in #{detail[:location][:file]}:" +
|
13
|
+
detail[:location][:line].to_s
|
14
|
+
else
|
15
|
+
detail[:text]
|
16
|
+
end
|
17
|
+
|
18
|
+
super args
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def attempt
|
23
|
+
ActiveSupport::Notifications.instrument('build.proscenium', identifier: path_to_build) do
|
24
|
+
render_response Proscenium::Esbuild.build(path_to_build, root: root,
|
25
|
+
base_url: @request.base_url)
|
26
|
+
end
|
27
|
+
rescue Proscenium::Esbuild::CompileError => e
|
28
|
+
raise self.class::CompileError, { file: @request.fullpath, detail: e.message }, caller
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
class Middleware
|
5
|
+
# Handles requests for URL encoded URL's.
|
6
|
+
class Url < Esbuild
|
7
|
+
private
|
8
|
+
|
9
|
+
# @override [Esbuild] It's a URL, so always assume it is renderable (we won't actually know
|
10
|
+
# until it's downloaded).
|
11
|
+
def renderable?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium
|
4
|
+
class Middleware
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
# Error when the build command fails.
|
8
|
+
class BuildError < StandardError; end
|
9
|
+
|
10
|
+
autoload :Base
|
11
|
+
autoload :Esbuild
|
12
|
+
autoload :Url
|
13
|
+
|
14
|
+
def initialize(app)
|
15
|
+
@app = app
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(env)
|
19
|
+
request = Rack::Request.new(env)
|
20
|
+
|
21
|
+
return @app.call(env) if !request.get? && !request.head?
|
22
|
+
|
23
|
+
attempt(request) || @app.call(env)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def attempt(request)
|
29
|
+
return unless (type = find_type(request))
|
30
|
+
|
31
|
+
# file_handler.attempt(request.env) || type.attempt(request)
|
32
|
+
|
33
|
+
type.attempt(request)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the type of file being requested using Proscenium::MIDDLEWARE_GLOB_TYPES.
|
37
|
+
def find_type(request)
|
38
|
+
path = Pathname.new(request.path)
|
39
|
+
|
40
|
+
return Url if request.path.match?(glob_types[:url])
|
41
|
+
return Esbuild if path.fnmatch?(application_glob_type, File::FNM_EXTGLOB)
|
42
|
+
end
|
43
|
+
|
44
|
+
# TODO: handle precompiled assets
|
45
|
+
def file_handler
|
46
|
+
::ActionDispatch::FileHandler.new Rails.public_path.join('assets').to_s,
|
47
|
+
headers: { 'X-Proscenium-Middleware' => 'precompiled' }
|
48
|
+
end
|
49
|
+
|
50
|
+
def glob_types
|
51
|
+
@glob_types ||= Proscenium::MIDDLEWARE_GLOB_TYPES
|
52
|
+
end
|
53
|
+
|
54
|
+
def application_glob_type
|
55
|
+
paths = Rails.application.config.proscenium.include_paths.join(',')
|
56
|
+
"/{#{paths}}#{glob_types[:application]}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Proscenium::Phlex::ComponentConcerns
|
4
|
+
module CssModules
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include Proscenium::CssModule
|
7
|
+
include Proscenium::Phlex::ResolveCssModules
|
8
|
+
|
9
|
+
# class_methods do
|
10
|
+
# # FIXME: Still needed?
|
11
|
+
# def path
|
12
|
+
# pp name, super
|
13
|
+
# pp Module.const_source_location(name).first
|
14
|
+
|
15
|
+
# name && Pathname.new(Module.const_source_location(name).first)
|
16
|
+
# rescue NameError
|
17
|
+
# nil
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def path
|
24
|
+
self.class.path
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'phlex/rails'
|
4
|
+
|
5
|
+
# Include this in your view for additional logic for rendering a full HTML page, usually from a
|
6
|
+
# controller.
|
7
|
+
module Proscenium::Phlex::Page
|
8
|
+
include Phlex::Rails::Helpers::CSPMetaTag
|
9
|
+
include Phlex::Rails::Helpers::CSRFMetaTags
|
10
|
+
include Phlex::Rails::Helpers::FaviconLinkTag
|
11
|
+
include Phlex::Rails::Helpers::PreloadLinkTag
|
12
|
+
include Phlex::Rails::Helpers::StyleSheetLinkTag
|
13
|
+
include Phlex::Rails::Helpers::ActionCableMetaTag
|
14
|
+
include Phlex::Rails::Helpers::AutoDiscoveryLinkTag
|
15
|
+
include Phlex::Rails::Helpers::JavaScriptIncludeTag
|
16
|
+
include Phlex::Rails::Helpers::JavaScriptImportMapTags
|
17
|
+
include Phlex::Rails::Helpers::JavaScriptImportModuleTag
|
18
|
+
|
19
|
+
def self.included(klass)
|
20
|
+
klass.extend(Phlex::Rails::Layout::Interface)
|
21
|
+
end
|
22
|
+
|
23
|
+
def template(&block)
|
24
|
+
doctype
|
25
|
+
html do
|
26
|
+
head
|
27
|
+
body(&block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def after_template
|
34
|
+
super
|
35
|
+
@_buffer.gsub!('<!-- [SIDE_LOAD_STYLESHEETS] -->', capture { side_load_stylesheets })
|
36
|
+
end
|
37
|
+
|
38
|
+
def page_title
|
39
|
+
Rails.application.class.name.deconstantize
|
40
|
+
end
|
41
|
+
|
42
|
+
def head
|
43
|
+
super do
|
44
|
+
title { page_title }
|
45
|
+
|
46
|
+
yield if block_given?
|
47
|
+
|
48
|
+
csp_meta_tag
|
49
|
+
csrf_meta_tags
|
50
|
+
|
51
|
+
comment { '[SIDE_LOAD_STYLESHEETS]' }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def body
|
56
|
+
super do
|
57
|
+
yield if block_given?
|
58
|
+
|
59
|
+
side_load_javascripts defer: true, type: :module
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Renders a div for use with @proscenium/component-manager.
|
5
|
+
#
|
6
|
+
# You can pass props to the component in the `:props` keyword argument.
|
7
|
+
#
|
8
|
+
# By default, the component is lazy loaded when intersecting using IntersectionObserver. Pass in
|
9
|
+
# :lazy as false to disable this and render the component immediately.
|
10
|
+
#
|
11
|
+
# React components are not side loaded at all.
|
12
|
+
#
|
13
|
+
class Proscenium::Phlex::ReactComponent < Phlex::HTML
|
14
|
+
class << self
|
15
|
+
attr_accessor :path, :abstract_class
|
16
|
+
|
17
|
+
def inherited(child)
|
18
|
+
position = caller_locations(1, 1).first.label == 'inherited' ? 2 : 1
|
19
|
+
child.path = Pathname.new caller_locations(position, 1).first.path.sub(/\.rb$/, '')
|
20
|
+
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
self.abstract_class = true
|
26
|
+
|
27
|
+
include Proscenium::Phlex::ComponentConcerns::CssModules
|
28
|
+
|
29
|
+
attr_writer :props, :lazy
|
30
|
+
|
31
|
+
# @param props: [Hash]
|
32
|
+
# @param lazy: [Boolean] Lazy load the component using IntersectionObserver. Default: true.
|
33
|
+
def initialize(props: {}, lazy: true) # rubocop:disable Lint/MissingSuper
|
34
|
+
@props = props
|
35
|
+
@lazy = lazy
|
36
|
+
end
|
37
|
+
|
38
|
+
# @yield the given block to a `div` within the top level component div. If not given,
|
39
|
+
# `<div>loading...</div>` will be rendered. Use this to display a loading UI while the component
|
40
|
+
# is loading and rendered.
|
41
|
+
def template(**attributes, &block)
|
42
|
+
component_root(:div, **attributes, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def component_root(element, **attributes, &block)
|
48
|
+
send element, data: { proscenium_component: component_data }, **attributes, &block
|
49
|
+
end
|
50
|
+
|
51
|
+
def props
|
52
|
+
@props ||= {}
|
53
|
+
end
|
54
|
+
|
55
|
+
def lazy
|
56
|
+
instance_variable_defined?(:@lazy) ? @lazy : (@lazy = false)
|
57
|
+
end
|
58
|
+
|
59
|
+
def component_data
|
60
|
+
{
|
61
|
+
path: virtual_path, lazy: lazy,
|
62
|
+
props: props.deep_transform_keys { |k| k.to_s.camelize :lower }
|
63
|
+
}.to_json
|
64
|
+
end
|
65
|
+
|
66
|
+
def virtual_path
|
67
|
+
path.to_s.delete_prefix(Rails.root.to_s)
|
68
|
+
end
|
69
|
+
end
|