proscenium 0.26.0-x64-mingw-ucrt

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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +84 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +1020 -0
  5. data/exe/proscenium +19 -0
  6. data/exe/psm +5 -0
  7. data/lib/generators/proscenium/bun/bun_generator.rb +150 -0
  8. data/lib/generators/proscenium/bun/templates/proscenium.preload.js +61 -0
  9. data/lib/proscenium/app_package.rb +90 -0
  10. data/lib/proscenium/builder.rb +403 -0
  11. data/lib/proscenium/bundled_gems.rb +140 -0
  12. data/lib/proscenium/cli/brackets.rb +66 -0
  13. data/lib/proscenium/cli/bun_linker.rb +79 -0
  14. data/lib/proscenium/cli/capabilities.json +29 -0
  15. data/lib/proscenium/cli/collisions.rb +94 -0
  16. data/lib/proscenium/cli/contexts.rb +133 -0
  17. data/lib/proscenium/cli/diff.rb +80 -0
  18. data/lib/proscenium/cli/error.rb +291 -0
  19. data/lib/proscenium/cli/gem_archive.rb +44 -0
  20. data/lib/proscenium/cli/gem_check.rb +150 -0
  21. data/lib/proscenium/cli/gitignore.rb +63 -0
  22. data/lib/proscenium/cli/inspect.rb +257 -0
  23. data/lib/proscenium/cli/install.rb +378 -0
  24. data/lib/proscenium/cli/manager.rb +323 -0
  25. data/lib/proscenium/cli/platform_variants.rb +82 -0
  26. data/lib/proscenium/cli/pnpm_approvals.rb +109 -0
  27. data/lib/proscenium/cli/pnpm_workspace.rb +149 -0
  28. data/lib/proscenium/cli/project_lock.rb +106 -0
  29. data/lib/proscenium/cli/registration.rb +140 -0
  30. data/lib/proscenium/cli/reporter.rb +103 -0
  31. data/lib/proscenium/cli/rules.rb +169 -0
  32. data/lib/proscenium/cli/runner.rb +142 -0
  33. data/lib/proscenium/cli/verify.rb +107 -0
  34. data/lib/proscenium/cli.rb +122 -0
  35. data/lib/proscenium/context_map.rb +181 -0
  36. data/lib/proscenium/css_module/path.rb +32 -0
  37. data/lib/proscenium/css_module/transformer.rb +118 -0
  38. data/lib/proscenium/css_module.rb +63 -0
  39. data/lib/proscenium/dependency_context.rb +74 -0
  40. data/lib/proscenium/ensure_loaded.rb +31 -0
  41. data/lib/proscenium/error.rb +10 -0
  42. data/lib/proscenium/ext/proscenium.dll +0 -0
  43. data/lib/proscenium/ext/proscenium.h +113 -0
  44. data/lib/proscenium/helper.rb +82 -0
  45. data/lib/proscenium/importer.rb +189 -0
  46. data/lib/proscenium/log_subscriber.rb +38 -0
  47. data/lib/proscenium/manifest.rb +48 -0
  48. data/lib/proscenium/mapping_generation.rb +64 -0
  49. data/lib/proscenium/middleware/base.rb +102 -0
  50. data/lib/proscenium/middleware/chunks.rb +46 -0
  51. data/lib/proscenium/middleware/esbuild.rb +11 -0
  52. data/lib/proscenium/middleware/ruby_gems.rb +37 -0
  53. data/lib/proscenium/middleware/silence_request.rb +31 -0
  54. data/lib/proscenium/middleware/vendor.rb +31 -0
  55. data/lib/proscenium/middleware.rb +67 -0
  56. data/lib/proscenium/monkey.rb +77 -0
  57. data/lib/proscenium/railtie.rb +106 -0
  58. data/lib/proscenium/railties/assets.rake +19 -0
  59. data/lib/proscenium/resolver.rb +67 -0
  60. data/lib/proscenium/runtime/bootstrap.js +266 -0
  61. data/lib/proscenium/runtime/bun.js +143 -0
  62. data/lib/proscenium/runtime/server.rb +702 -0
  63. data/lib/proscenium/side_load.rb +210 -0
  64. data/lib/proscenium/source_path.rb +15 -0
  65. data/lib/proscenium/stale_contexts.rb +118 -0
  66. data/lib/proscenium/templates/rescues/build_error.html.erb +32 -0
  67. data/lib/proscenium/utils.rb +104 -0
  68. data/lib/proscenium/version.rb +5 -0
  69. data/lib/proscenium/yaml_tree.rb +48 -0
  70. data/lib/proscenium.rb +87 -0
  71. 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium
4
+ class Middleware
5
+ class Esbuild < Base
6
+ def attempt
7
+ render_response Builder.build_to_string(path_to_build)
8
+ end
9
+ end
10
+ end
11
+ 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