proscenium 0.26.0-aarch64-linux-gnu
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/LICENSE.txt +21 -0
- data/README.md +1020 -0
- data/exe/proscenium +19 -0
- data/exe/psm +5 -0
- data/lib/generators/proscenium/bun/bun_generator.rb +150 -0
- data/lib/generators/proscenium/bun/templates/proscenium.preload.js +61 -0
- data/lib/proscenium/app_package.rb +90 -0
- data/lib/proscenium/builder.rb +403 -0
- data/lib/proscenium/bundled_gems.rb +140 -0
- data/lib/proscenium/cli/brackets.rb +66 -0
- data/lib/proscenium/cli/bun_linker.rb +79 -0
- data/lib/proscenium/cli/capabilities.json +29 -0
- data/lib/proscenium/cli/collisions.rb +94 -0
- data/lib/proscenium/cli/contexts.rb +133 -0
- data/lib/proscenium/cli/diff.rb +80 -0
- data/lib/proscenium/cli/error.rb +291 -0
- data/lib/proscenium/cli/gem_archive.rb +44 -0
- data/lib/proscenium/cli/gem_check.rb +150 -0
- data/lib/proscenium/cli/gitignore.rb +63 -0
- data/lib/proscenium/cli/inspect.rb +257 -0
- data/lib/proscenium/cli/install.rb +378 -0
- data/lib/proscenium/cli/manager.rb +323 -0
- data/lib/proscenium/cli/platform_variants.rb +82 -0
- data/lib/proscenium/cli/pnpm_approvals.rb +109 -0
- data/lib/proscenium/cli/pnpm_workspace.rb +149 -0
- data/lib/proscenium/cli/project_lock.rb +106 -0
- data/lib/proscenium/cli/registration.rb +140 -0
- data/lib/proscenium/cli/reporter.rb +103 -0
- data/lib/proscenium/cli/rules.rb +169 -0
- data/lib/proscenium/cli/runner.rb +142 -0
- data/lib/proscenium/cli/verify.rb +107 -0
- data/lib/proscenium/cli.rb +122 -0
- data/lib/proscenium/context_map.rb +181 -0
- data/lib/proscenium/css_module/path.rb +32 -0
- data/lib/proscenium/css_module/transformer.rb +118 -0
- data/lib/proscenium/css_module.rb +63 -0
- data/lib/proscenium/dependency_context.rb +74 -0
- data/lib/proscenium/ensure_loaded.rb +31 -0
- data/lib/proscenium/error.rb +10 -0
- data/lib/proscenium/ext/proscenium +0 -0
- data/lib/proscenium/ext/proscenium.h +113 -0
- data/lib/proscenium/helper.rb +82 -0
- data/lib/proscenium/importer.rb +189 -0
- data/lib/proscenium/log_subscriber.rb +38 -0
- data/lib/proscenium/manifest.rb +48 -0
- data/lib/proscenium/mapping_generation.rb +64 -0
- data/lib/proscenium/middleware/base.rb +102 -0
- data/lib/proscenium/middleware/chunks.rb +46 -0
- data/lib/proscenium/middleware/esbuild.rb +11 -0
- data/lib/proscenium/middleware/ruby_gems.rb +37 -0
- data/lib/proscenium/middleware/silence_request.rb +31 -0
- data/lib/proscenium/middleware/vendor.rb +31 -0
- data/lib/proscenium/middleware.rb +67 -0
- data/lib/proscenium/monkey.rb +77 -0
- data/lib/proscenium/railtie.rb +106 -0
- data/lib/proscenium/railties/assets.rake +19 -0
- data/lib/proscenium/resolver.rb +67 -0
- data/lib/proscenium/runtime/bootstrap.js +266 -0
- data/lib/proscenium/runtime/bun.js +143 -0
- data/lib/proscenium/runtime/server.rb +702 -0
- data/lib/proscenium/side_load.rb +210 -0
- data/lib/proscenium/source_path.rb +15 -0
- data/lib/proscenium/stale_contexts.rb +118 -0
- data/lib/proscenium/templates/rescues/build_error.html.erb +32 -0
- data/lib/proscenium/utils.rb +104 -0
- data/lib/proscenium/version.rb +5 -0
- data/lib/proscenium/yaml_tree.rb +48 -0
- data/lib/proscenium.rb +87 -0
- metadata +148 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proscenium
|
|
4
|
+
class Middleware
|
|
5
|
+
class Base
|
|
6
|
+
include ActiveSupport::Benchmarkable
|
|
7
|
+
|
|
8
|
+
def self.attempt(request)
|
|
9
|
+
new(request).renderable!&.attempt
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(request)
|
|
13
|
+
@request = request
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def renderable!
|
|
17
|
+
# The single funnel every subclass reaches, so the guard sits here rather than in
|
|
18
|
+
# `renderable?` - a subclass that overrides that (RubyGems does) cannot skip it.
|
|
19
|
+
return unless normalised_path
|
|
20
|
+
|
|
21
|
+
renderable? ? self : nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# The request path, decoded and with `.` and `..` segments resolved. Everything below is
|
|
27
|
+
# derived from this one value; see `Middleware.normalise_path` for why that matters.
|
|
28
|
+
def normalised_path
|
|
29
|
+
return @normalised_path if defined?(@normalised_path)
|
|
30
|
+
|
|
31
|
+
@normalised_path = Middleware.normalise_path(@request.path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def real_path
|
|
35
|
+
@real_path ||= normalised_path
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [String] the path to the file without the leading slash which will be built.
|
|
39
|
+
def path_to_build
|
|
40
|
+
@path_to_build ||= normalised_path[1..]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def sourcemap?
|
|
44
|
+
normalised_path.ends_with?('.map')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def renderable?
|
|
48
|
+
file_readable?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Probes the path exactly as it will be built. `real_path` is already decoded and cleaned, so
|
|
52
|
+
# decoding it again here - as this once did - probed a different file than the one built
|
|
53
|
+
# whenever the request was double-encoded: `%252e%252e` was built as a literal `%2e%2e`
|
|
54
|
+
# segment but probed as `..`.
|
|
55
|
+
def file_readable?
|
|
56
|
+
path = sourcemap? ? real_path[0...-4] : real_path
|
|
57
|
+
file_stat = File.stat(root_for_readable.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 root_for_readable
|
|
65
|
+
Rails.root
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def content_type
|
|
69
|
+
case ::File.extname(path_to_build)
|
|
70
|
+
when '.js', '.mjs', '.ts', '.tsx', '.jsx' then 'application/javascript'
|
|
71
|
+
when '.css' then 'text/css'
|
|
72
|
+
when '.map' then 'application/json'
|
|
73
|
+
else
|
|
74
|
+
::Rack::Mime.mime_type(::File.extname(path_to_build), nil) || 'application/javascript'
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def render_response(result)
|
|
79
|
+
content = result[:response]
|
|
80
|
+
|
|
81
|
+
response = Rack::Response.new
|
|
82
|
+
response['X-Proscenium-Middleware'] = name
|
|
83
|
+
response.set_header 'SourceMap', "#{@request.path_info}.map"
|
|
84
|
+
response.content_type = content_type
|
|
85
|
+
response.etag = result[:content_hash]
|
|
86
|
+
|
|
87
|
+
if @request.fresh?(response)
|
|
88
|
+
response.status = 304
|
|
89
|
+
response.body = []
|
|
90
|
+
else
|
|
91
|
+
response.write content
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
response.finish
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def name
|
|
98
|
+
@name ||= self.class.name.split('::').last.downcase
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proscenium
|
|
4
|
+
class Middleware
|
|
5
|
+
class Chunks
|
|
6
|
+
def initialize(app)
|
|
7
|
+
@app = app
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(env)
|
|
11
|
+
request = ActionDispatch::Request.new(env)
|
|
12
|
+
|
|
13
|
+
# Decode and clean the path BEFORE deciding anything about it, then hand that same path
|
|
14
|
+
# to the file handler. `ActionDispatch::FileHandler` normalises `PATH_INFO` itself before
|
|
15
|
+
# it resolves a file, so a guard reading the raw path is not looking at the path that
|
|
16
|
+
# gets served: `/_asset_chunks/x-$FAKE$/%2e%2e/real-$ABC123$.js` passed both checks and
|
|
17
|
+
# then served `real-$ABC123$.js` tagged `FAKE`, and `x-$FAKE$/../../lib/foo.js` left the
|
|
18
|
+
# chunk directory altogether, since the handler's root is the whole output directory.
|
|
19
|
+
# Both under `immutable, max-age=100.years`, so a wrong ETag is not something a client
|
|
20
|
+
# can clear.
|
|
21
|
+
#
|
|
22
|
+
# The path also has to yield a content hash, not just match the prefix - `CHUNKS_PATH`
|
|
23
|
+
# checks only the latter, and `/_asset_chunks/nohash.js` used to index the nil match and
|
|
24
|
+
# return a 500 from here.
|
|
25
|
+
#
|
|
26
|
+
# FileHandler decodes `PATH_INFO` again, and that cannot be switched off, so a path still
|
|
27
|
+
# holding a `%` after the first decode is refused: the handler would resolve it to a
|
|
28
|
+
# different file than the one checked here. esbuild never puts a `%` in a chunk name.
|
|
29
|
+
return @app.call(env) unless (path = Middleware.normalise_path(request.path)) &&
|
|
30
|
+
!path.include?('%') &&
|
|
31
|
+
path.match?(CHUNKS_PATH) &&
|
|
32
|
+
(etag = path[/-\$([a-z0-9]+)\$/i, 1])
|
|
33
|
+
|
|
34
|
+
ActionDispatch::FileHandler.new(
|
|
35
|
+
Proscenium.config.output_path.to_s,
|
|
36
|
+
headers: {
|
|
37
|
+
'X-Proscenium-Middleware' => 'chunks',
|
|
38
|
+
'SourceMap' => "#{path}.map",
|
|
39
|
+
'Cache-Control' => "public, max-age=#{100.years}, immutable",
|
|
40
|
+
'ETag' => etag
|
|
41
|
+
}
|
|
42
|
+
).attempt(env.merge('PATH_INFO' => path)) || @app.call(env)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proscenium
|
|
4
|
+
class Middleware
|
|
5
|
+
class RubyGems < Esbuild
|
|
6
|
+
def real_path
|
|
7
|
+
@real_path ||= Pathname.new(gem_request_path.delete_prefix("#{gem_name}/")).to_s
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# An unknown gem is "not mine", the same answer a missing file under an app path gets from
|
|
11
|
+
# `Base#file_readable?`. Without this, `pathname_for!` raised out of the readability probe
|
|
12
|
+
# and a request for a gem that is not in the Gemfile 500'd, while the sibling app-path
|
|
13
|
+
# branch quietly passed the same shape of miss down the stack.
|
|
14
|
+
def renderable?
|
|
15
|
+
BundledGems.pathname_for(gem_name) && super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def root_for_readable
|
|
19
|
+
BundledGems.pathname_for!(gem_name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def gem_name
|
|
23
|
+
@gem_name ||= gem_request_path.split('/').first
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Taken from the normalised path, so the gem name and the suffix describe the file that
|
|
27
|
+
# will actually be built. Read from the request verbatim, `@rubygems/gem1/../index.js`
|
|
28
|
+
# named gem1 and a suffix the readability probe then cleaned to `index.js` - approving
|
|
29
|
+
# `<gem1>/index.js` while the builder resolved `../index.js` and served the file BESIDE
|
|
30
|
+
# the gem root. Normalised, it names a gem called `index.js`, which is not bundled, so
|
|
31
|
+
# the request passes through.
|
|
32
|
+
def gem_request_path
|
|
33
|
+
@gem_request_path ||= normalised_path.delete_prefix('/node_modules/@rubygems/')
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/logger_silence'
|
|
4
|
+
|
|
5
|
+
module Proscenium
|
|
6
|
+
class Middleware::SilenceRequest
|
|
7
|
+
def initialize(app)
|
|
8
|
+
@app = app
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(env)
|
|
12
|
+
request = ActionDispatch::Request.new(env)
|
|
13
|
+
|
|
14
|
+
if (request.get? || request.head?) && proscenium_request?(request)
|
|
15
|
+
Rails.logger.silence { @app.call(env) }
|
|
16
|
+
else
|
|
17
|
+
@app.call(env)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def proscenium_request?(request)
|
|
24
|
+
return true if request.path.match?(CHUNKS_PATH)
|
|
25
|
+
|
|
26
|
+
pathname = Pathname.new(request.path)
|
|
27
|
+
pathname.fnmatch?(GEMS_PATH_GLOB, File::FNM_EXTGLOB) ||
|
|
28
|
+
pathname.fnmatch?(APP_PATH_GLOB, File::FNM_EXTGLOB)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proscenium
|
|
4
|
+
class Middleware
|
|
5
|
+
class Vendor
|
|
6
|
+
def initialize(app)
|
|
7
|
+
@app = app
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(env)
|
|
11
|
+
request = ActionDispatch::Request.new(env)
|
|
12
|
+
pathname = Pathname.new(request.path)
|
|
13
|
+
|
|
14
|
+
return @app.call(env) unless pathname.fnmatch?(VENDOR_PATH_GLOB, File::FNM_EXTGLOB)
|
|
15
|
+
|
|
16
|
+
# The prefix strip is an argument to the file lookup, not a rewrite of the request. It used
|
|
17
|
+
# to assign `request.path_info`, which writes `env['PATH_INFO']` - the same hash handed to
|
|
18
|
+
# `@app.call` on a miss - so `/vendor/lib/foo.js` with nothing under `vendor/` continued
|
|
19
|
+
# down the stack as `/lib/foo.js`, matched `APP_PATH_GLOB`, and was built and served from
|
|
20
|
+
# the app root under the URL the client had actually asked for.
|
|
21
|
+
ActionDispatch::FileHandler.new(
|
|
22
|
+
Rails.root.join('vendor').to_s,
|
|
23
|
+
headers: {
|
|
24
|
+
'X-Proscenium-Middleware' => 'vendor',
|
|
25
|
+
'Cache-Control' => "public, max-age=#{100.years}, immutable"
|
|
26
|
+
}
|
|
27
|
+
).attempt(env.merge('PATH_INFO' => request.path.delete_prefix('/vendor'))) || @app.call(env)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proscenium
|
|
4
|
+
class Middleware
|
|
5
|
+
extend ActiveSupport::Autoload
|
|
6
|
+
|
|
7
|
+
class BuildError < Error; end
|
|
8
|
+
|
|
9
|
+
autoload :Base
|
|
10
|
+
autoload :Esbuild
|
|
11
|
+
autoload :RubyGems
|
|
12
|
+
autoload :Vendor
|
|
13
|
+
autoload :Chunks
|
|
14
|
+
autoload :SilenceRequest
|
|
15
|
+
|
|
16
|
+
# The given request path, percent-decoded and with `.` and `..` segments resolved. Nil for a
|
|
17
|
+
# path Rack rejects outright, such as one containing a null byte.
|
|
18
|
+
#
|
|
19
|
+
# Every decision and every derived path in this subsystem is taken from this one form. The
|
|
20
|
+
# alternative was three derivations from the raw path that disagreed: `find_type` matched the
|
|
21
|
+
# allowed-directory glob against the request verbatim, `Base#file_readable?` normalised before
|
|
22
|
+
# probing the disk, and `Base#path_to_build` kept the request verbatim again - so a request
|
|
23
|
+
# could be routed by a directory it does not resolve to, approved against one file, and built
|
|
24
|
+
# as a third.
|
|
25
|
+
def self.normalise_path(path)
|
|
26
|
+
unescaped = Rack::Utils.unescape_path(path.chomp('/').delete_prefix('/'))
|
|
27
|
+
return unless Rack::Utils.valid_path?(unescaped)
|
|
28
|
+
|
|
29
|
+
"/#{Rack::Utils.clean_path_info(unescaped)}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(app)
|
|
33
|
+
@app = app
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call(env)
|
|
37
|
+
request = ActionDispatch::Request.new(env)
|
|
38
|
+
|
|
39
|
+
return @app.call(env) if !request.get? && !request.head?
|
|
40
|
+
|
|
41
|
+
attempt(request) || @app.call(env)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def attempt(request)
|
|
47
|
+
return unless (type = find_type(request))
|
|
48
|
+
|
|
49
|
+
type.attempt(request)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def find_type(request)
|
|
53
|
+
# Matched against the normalised path, not the request, or the allowed-directory list is
|
|
54
|
+
# bypassable: `/lib/../public/x.js` matches `APP_PATH_GLOB` as written and resolves to
|
|
55
|
+
# `public/`, which is not an allowed directory - and it was built and served from there.
|
|
56
|
+
return unless (path = Middleware.normalise_path(request.path))
|
|
57
|
+
|
|
58
|
+
pathname = Pathname.new(path)
|
|
59
|
+
|
|
60
|
+
if pathname.fnmatch?(GEMS_PATH_GLOB, File::FNM_EXTGLOB)
|
|
61
|
+
RubyGems
|
|
62
|
+
elsif pathname.fnmatch?(APP_PATH_GLOB, File::FNM_EXTGLOB)
|
|
63
|
+
Esbuild
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proscenium
|
|
4
|
+
module Monkey
|
|
5
|
+
# A template ActionView pushes onto `@current_template` is not the body of a partial rendered
|
|
6
|
+
# with a block, so `Helper#sideload_assets` must not see that partial while one renders. Rails
|
|
7
|
+
# caches template objects, so the pushed template can be the very one that rendered the partial.
|
|
8
|
+
#
|
|
9
|
+
# Overrides ActionView's private `ActionView::Base#_run(method, template, locals, buffer,
|
|
10
|
+
# add_to_stack: true, ...)`, unchanged from Rails 7.2 to 8.1. `@proscenium_block_partial` is
|
|
11
|
+
# set by `Helper#proscenium_render_block_partial`. Every template render runs through here, so
|
|
12
|
+
# arguments are forwarded with `...`, which allocates nothing. Reading `add_to_stack` does
|
|
13
|
+
# allocate, so it is only read while a block partial is rendering.
|
|
14
|
+
module Base
|
|
15
|
+
def _run(...)
|
|
16
|
+
return super unless @proscenium_block_partial && proscenium_pushes_template?(...)
|
|
17
|
+
|
|
18
|
+
owner = @proscenium_block_partial
|
|
19
|
+
@proscenium_block_partial = nil
|
|
20
|
+
super
|
|
21
|
+
ensure
|
|
22
|
+
@proscenium_block_partial = owner if owner
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def proscenium_pushes_template?(*, add_to_stack: true, **) = add_to_stack
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module TemplateRenderer
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def render_template(view, template, layout_name, locals)
|
|
34
|
+
return super unless SideLoad.sideloadable?(view, template)
|
|
35
|
+
|
|
36
|
+
layout = find_layout(layout_name, locals.keys, [formats.first])
|
|
37
|
+
SideLoad.sideload_templates(view, [layout, template]) { super }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module PartialRenderer
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def render_partial_template(view, locals, template, layout, block)
|
|
45
|
+
return super unless SideLoad.sideloadable?(view, template)
|
|
46
|
+
|
|
47
|
+
SideLoad.sideload_templates(view, [layout, template]) do
|
|
48
|
+
# A view rendered before Proscenium::Helper is included cannot call `sideload_assets`.
|
|
49
|
+
if block && view.respond_to?(:proscenium_render_block_partial)
|
|
50
|
+
view.proscenium_render_block_partial(template, block) do |wrapped|
|
|
51
|
+
super(view, locals, template, layout, wrapped)
|
|
52
|
+
end
|
|
53
|
+
else
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A collection renders each item with `Template#render`, so never reaches
|
|
61
|
+
# `PartialRenderer#render_partial_template`. Hooked above the collection cache, which skips
|
|
62
|
+
# rendering entirely on a full hit. A mixed collection has no single template, and is not side
|
|
63
|
+
# loaded.
|
|
64
|
+
module CollectionRenderer
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# `args` is `path, template, layout, block`. `collection` is ActionView's CollectionIterator,
|
|
68
|
+
# which has `length` but no `empty?`.
|
|
69
|
+
def render_collection(collection, view, *args)
|
|
70
|
+
_path, template, layout, = args
|
|
71
|
+
return super unless collection.length.positive? && SideLoad.sideloadable?(view, template)
|
|
72
|
+
|
|
73
|
+
SideLoad.sideload_templates(view, [layout, template]) { super }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails'
|
|
4
|
+
|
|
5
|
+
ENV['RAILS_ENV'] = Rails.env
|
|
6
|
+
|
|
7
|
+
module Proscenium
|
|
8
|
+
class Railtie < ::Rails::Engine
|
|
9
|
+
isolate_namespace Proscenium
|
|
10
|
+
|
|
11
|
+
config.proscenium = ActiveSupport::OrderedOptions.new
|
|
12
|
+
config.proscenium.debug = false
|
|
13
|
+
config.proscenium.logging = true
|
|
14
|
+
config.proscenium.bundle = true
|
|
15
|
+
config.proscenium.side_load = true
|
|
16
|
+
config.proscenium.code_splitting = true
|
|
17
|
+
config.proscenium.ensure_loaded = :raise
|
|
18
|
+
config.proscenium.aliases = {}
|
|
19
|
+
config.proscenium.external = Set['*.rjs', '*.gif', '*.jpg', '*.png', '*.woff2', '*.woff']
|
|
20
|
+
config.proscenium.precompile = Set.new
|
|
21
|
+
config.proscenium.output_dir = '/assets'
|
|
22
|
+
|
|
23
|
+
# List of environment variable names that should be passed to the builder, which will then be
|
|
24
|
+
# passed to esbuild's `Define` option. Being explicit about which environment variables are
|
|
25
|
+
# defined means a faster build, as esbuild will have less to do.
|
|
26
|
+
config.proscenium.env_vars = Set.new
|
|
27
|
+
|
|
28
|
+
config.action_dispatch.rescue_templates = {
|
|
29
|
+
'Proscenium::Builder::BuildError' => 'build_error'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
config.after_initialize do |app|
|
|
33
|
+
config.proscenium.output_path ||=
|
|
34
|
+
Pathname.new(File.join(app.config.paths['public'].first, app.config.proscenium.output_dir))
|
|
35
|
+
config.proscenium.manifest_path = config.proscenium.output_path.join('.manifest.json')
|
|
36
|
+
|
|
37
|
+
Proscenium::Manifest.load!
|
|
38
|
+
|
|
39
|
+
project = Proscenium::ContextMap.project_root
|
|
40
|
+
if (notice = Proscenium::ContextMap.adoption_notice(project))
|
|
41
|
+
Rails.logger.info "[Proscenium] #{notice}"
|
|
42
|
+
end
|
|
43
|
+
# Production finds stale contexts at assets:precompile, which refuses to compile. Elsewhere
|
|
44
|
+
# the first mapping generation starts here, so a change after boot, such as running
|
|
45
|
+
# `proscenium install`, starts the next one. No Go call: Puma may fork after boot.
|
|
46
|
+
unless Rails.env.production?
|
|
47
|
+
generation, = Proscenium::MappingGeneration.refresh(project)
|
|
48
|
+
if (stale = Proscenium::StaleContexts.message(project, generation))
|
|
49
|
+
Rails.logger.warn "[Proscenium] #{stale}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if config.proscenium.logging
|
|
54
|
+
require 'proscenium/log_subscriber'
|
|
55
|
+
Proscenium::LogSubscriber.attach_to :proscenium
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
ActiveSupport.on_load(:action_view) do
|
|
59
|
+
include Proscenium::Helper
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
initializer 'proscenium.debugging' do
|
|
64
|
+
tpl_path = root.join('lib', 'proscenium', 'templates').to_s
|
|
65
|
+
ActionDispatch::DebugView::RESCUES_TEMPLATE_PATHS << tpl_path
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
initializer 'proscenium.middleware' do |app|
|
|
69
|
+
unless config.proscenium.logging
|
|
70
|
+
app.middleware.insert_before Rails::Rack::Logger, Proscenium::Middleware::SilenceRequest
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
app.middleware.insert_after Rack::Sendfile, Proscenium::Middleware::Vendor
|
|
74
|
+
app.middleware.insert_after Rack::Sendfile, Proscenium::Middleware::Chunks
|
|
75
|
+
|
|
76
|
+
# Ensure the middleware is inserted as early as possible.
|
|
77
|
+
if app.config.consider_all_requests_local
|
|
78
|
+
app.middleware.insert_before ActionDispatch::ActionableExceptions, Proscenium::Middleware
|
|
79
|
+
elsif app.config.reloading_enabled?
|
|
80
|
+
app.middleware.insert_before ActionDispatch::Reloader, Proscenium::Middleware
|
|
81
|
+
else
|
|
82
|
+
app.middleware.insert_before ActionDispatch::Callbacks, Proscenium::Middleware
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
initializer 'proscenium.sideloading' do
|
|
87
|
+
ActiveSupport.on_load(:action_controller) do
|
|
88
|
+
ActionController::Base.include EnsureLoaded
|
|
89
|
+
ActionController::Base.include SideLoad::Controller
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
initializer 'proscenium.monkey_patches' do
|
|
94
|
+
ActiveSupport.on_load(:action_view) do
|
|
95
|
+
ActionView::Base.prepend Monkey::Base
|
|
96
|
+
ActionView::TemplateRenderer.prepend Monkey::TemplateRenderer
|
|
97
|
+
ActionView::PartialRenderer.prepend Monkey::PartialRenderer
|
|
98
|
+
ActionView::CollectionRenderer.prepend Monkey::CollectionRenderer
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
rake_tasks do
|
|
103
|
+
load 'proscenium/railties/assets.rake'
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :assets do
|
|
4
|
+
desc 'Compile Proscenium assets'
|
|
5
|
+
task precompile: :environment do
|
|
6
|
+
puts "\nPre-compiling assets..."
|
|
7
|
+
|
|
8
|
+
# Raises Proscenium::Builder::CompileError, with esbuild's messages, when the build fails.
|
|
9
|
+
Proscenium::Builder.compile
|
|
10
|
+
|
|
11
|
+
puts "\nAssets pre-compiled successfully."
|
|
12
|
+
|
|
13
|
+
if Rails.env.development?
|
|
14
|
+
puts "\nWarning: You are precompiling assets in development. Rails will not " \
|
|
15
|
+
'serve any changed assets until you delete ' \
|
|
16
|
+
"public#{Rails.application.config.proscenium.output_dir}/.manifest.json"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proscenium
|
|
4
|
+
class Resolver
|
|
5
|
+
mattr_accessor :resolved, instance_accessor: false, default: {}
|
|
6
|
+
|
|
7
|
+
# Resolve the given `path` to a fully qualified URL path.
|
|
8
|
+
#
|
|
9
|
+
# @param path [String] URL path, file system path, or bare specifier (ie. NPM package).
|
|
10
|
+
# @param as_array [Boolean] whether or not to return the manifest path, non-manifest path, and
|
|
11
|
+
# absolute file system path as an array. Only returns the resolved path if false (default).
|
|
12
|
+
# @return [String, Array<String>]
|
|
13
|
+
def self.resolve(path, as_array: false)
|
|
14
|
+
# Normalised before anything reads it, the guard and the cache key included. The bun test
|
|
15
|
+
# harness hands this Bun's own spelling of a module path, which on Windows is `D:\...`;
|
|
16
|
+
# matched against a slash-form Rails.root it fell through to the Go resolver and came back
|
|
17
|
+
# as the url path unchanged - a backslash path the harness then refused to build.
|
|
18
|
+
path = Utils.fs_path(path)
|
|
19
|
+
|
|
20
|
+
if path.start_with?('./', '../')
|
|
21
|
+
raise ArgumentError, '`path` must be an absolute file system or URL path'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A new mapping generation forgets cached resolutions (#154): polled here too, so a path
|
|
25
|
+
# resolved before an install does not keep its old URL when no Builder is made after it.
|
|
26
|
+
generation
|
|
27
|
+
|
|
28
|
+
# Caches the manifest key, not its value, so a manifest loaded or reset later is still
|
|
29
|
+
# honoured. Every string is frozen: the URL path is handed back to callers, and in the
|
|
30
|
+
# Rails.root branch it is the key itself, so mutating it would redirect later lookups.
|
|
31
|
+
key, url_path, abs_path = resolved[path] ||= entry_for(path).map(&:-@).freeze
|
|
32
|
+
manifest_path = Proscenium::Manifest[key]
|
|
33
|
+
|
|
34
|
+
as_array ? [manifest_path, url_path, abs_path] : manifest_path || url_path
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The manifest key, URL path and absolute file system path for `path`. In the gem branch the
|
|
38
|
+
# key differs from the URL path, so it is kept rather than derived.
|
|
39
|
+
def self.entry_for(path)
|
|
40
|
+
if (vpath = BundledGems.virtual_path(path))
|
|
41
|
+
[vpath, "/node_modules/#{vpath}", path]
|
|
42
|
+
elsif path.start_with?("#{Rails.root}/")
|
|
43
|
+
vpath = path.delete_prefix(Rails.root.to_s)
|
|
44
|
+
[vpath, vpath, path]
|
|
45
|
+
else
|
|
46
|
+
[path, *Builder.resolve(path)]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
private_class_method :entry_for
|
|
50
|
+
|
|
51
|
+
# The mapping generation builds and resolutions use now (#154). Development and test start a
|
|
52
|
+
# new one when the files the context map is built from change, and forget resolved paths with
|
|
53
|
+
# it; production keeps one for the process. Here rather than on Builder, which loads the Go
|
|
54
|
+
# library when first referenced, so resolving a path never needs it.
|
|
55
|
+
def self.generation
|
|
56
|
+
return 0 if Rails.env.production?
|
|
57
|
+
|
|
58
|
+
number, changed = MappingGeneration.refresh(ContextMap.project_root)
|
|
59
|
+
reset if changed
|
|
60
|
+
number
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.reset
|
|
64
|
+
self.resolved = {}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|