proscenium 0.21.6-arm64-darwin → 0.22.0-arm64-darwin

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bafa3c4ef5b5b97a3a1049ee46f1a1c725dc2191158dd84869b189ee64409c03
4
- data.tar.gz: 91625ccd60bbc983dd99dd2368ee3422e0e9b12593e5cb2b207d8f4e6b4a2591
3
+ metadata.gz: 223869aa06c452bb332f16d9be4254ac0a7e542da2666879164c3f5fd93344e4
4
+ data.tar.gz: bd6e10dc0a32400009a74628a6ec2a5bee1cdbed99839e9ca1a1978a7e60243a
5
5
  SHA512:
6
- metadata.gz: e58c3404b1687bcdef9e59b0f8f5d291db9cb1bdffd805c2c86c54259e8eac4b4615266a79863e238e778d83a1166cc3b79a739e418c8b4e48468b88d51d97ff
7
- data.tar.gz: 2172db0bc1b008e011baa6b16eeb50b4e742ab532572d8f0c2d684b698a927b88ae2c310264ef6d9ed4560b9652a1a606e4717e175b86fb7fbc269312f62e10c
6
+ metadata.gz: 541fe7b47d257d2ba454c50a2ae587d971c610b583a8e7468862a25279029ea71cf1d722918b75131b640859a534a4dfc5af83db71c90da75380e97b7839d7f8
7
+ data.tar.gz: 9cae265e01be8700e82a5ff56b175b1a123d7f7304014211e423d31936bcad196a76947ae4497c0d49db5f494b356cbdc8b9efe39a9ce35ca92dfbb58046c3d6
data/README.md CHANGED
@@ -18,7 +18,6 @@
18
18
  - Deep integration with Rails.
19
19
  - Automatically side-load JS and CSS for your layouts, views, and partials.
20
20
  - Import from NPM, URL's, and locally.
21
- - Server-side import map support.
22
21
  - CSS Modules & mixins.
23
22
  - Source maps.
24
23
 
@@ -30,7 +29,6 @@
30
29
  - [Side Loading](#side-loading)
31
30
  - [Importing](#importing-assets)
32
31
  - [Local Imports](#local-imports)
33
- - [Import Maps](#import-maps)
34
32
  - [Source Maps](#source-maps)
35
33
  - [SVG](#svg)
36
34
  - [Environment Variables](#environment-variables)
@@ -48,9 +46,10 @@
48
46
  - [Typescript Caveats](#typescript-caveats)
49
47
  - [JSX](#jsx)
50
48
  - [JSON](#json)
51
- - [Cache Busting](#cache-busting)
52
49
  - [rjs is back!](#rjs-is-back)
53
50
  - [Resolution](#resolution)
51
+ - [Aliases](#aliases)
52
+ - [Pre-compilation](#precompilation)
54
53
  - [Thanks](#thanks)
55
54
  - [Development](#development)
56
55
 
@@ -258,13 +257,11 @@ Sometimes you don't want to bundle an import. For example, you want to ensure th
258
257
  import React from "react" with { unbundle: 'true' };
259
258
  ```
260
259
 
261
- You can also unbundle entries in your [import map](#import-maps) using an `unbundle:` prefix, which ensures that all imports of a particular path are always unbundled:
260
+ You can also unbundle entries in [`aliases`](#aliases) using an `unbundle:` prefix, which ensures that all imports of a particular path are always unbundled:
262
261
 
263
- ```json
264
- {
265
- "imports": {
266
- "react": "unbundle:react"
267
- }
262
+ ```ruby
263
+ config.proscenium.aliases = {
264
+ "react": "unbundle:react"
268
265
  }
269
266
  ```
270
267
 
@@ -282,55 +279,6 @@ config.proscenium.bundle = false
282
279
 
283
280
  This will mean every asset and import will be loaded independently.
284
281
 
285
- ## Import Maps
286
-
287
- > **[WIP]**
288
-
289
- [Import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) for both JS and CSS is supported out of the box, and works with no regard to the browser being used. This is because the import map is parsed and resolved by Proscenium on the server, instead of by the browser. This is faster, and also allows you to use import maps in browsers that do not support them yet.
290
-
291
- If you are not familiar with import maps, think of them as a way to define aliases.
292
-
293
- Just create `config/import_map.json` and specify the imports you want to use. For example:
294
-
295
- ```json
296
- {
297
- "imports": {
298
- "react": "https://esm.sh/react@18.2.0",
299
- "start": "/lib/start.js",
300
- "common": "/lib/common.css",
301
- "@radix-ui/colors/": "https://esm.sh/@radix-ui/colors@0.1.8/"
302
- }
303
- }
304
- ```
305
-
306
- Using the above import map, we can do...
307
-
308
- ```js
309
- import { useCallback } from "react";
310
- import startHere from "start";
311
- import styles from "common";
312
- ```
313
-
314
- and for CSS...
315
-
316
- ```css
317
- @import "common";
318
- @import "@radix-ui/colors/blue.css";
319
- ```
320
-
321
- You can also write your import map in JavaScript instead of JSON. So instead of `config/import_map.json`, create `config/import_map.js`, and define an anonymous function. This function accepts a single `environment` argument.
322
-
323
- ```js
324
- (env) => ({
325
- imports: {
326
- react:
327
- env === "development"
328
- ? "https://esm.sh/react@18.2.0?dev"
329
- : "https://esm.sh/react@18.2.0",
330
- },
331
- });
332
- ```
333
-
334
282
  ## Source Maps
335
283
 
336
284
  Source maps can make it easier to debug your code. They encode the information necessary to translate from a line/column offset in a generated output file back to a line/column offset in the corresponding original input file. This is useful if your generated code is sufficiently different from your original code (e.g. your original code is TypeScript or you enabled minification). This is also useful if you prefer looking at individual files in your browser's developer tools instead of one big bundled file.
@@ -692,39 +640,59 @@ import { version } from "./package.json";
692
640
  console.log(version);
693
641
  ```
694
642
 
695
- ## Cache Busting
643
+ ## rjs is back
696
644
 
697
- By default, all assets are not cached by the browser. But if in production, you populate the `REVISION` env variable, all CSS and JS URL's will be appended with its value as a query string, and the `Cache-Control` response header will be set to `public` and a max-age of 30 days.
645
+ Proscenium brings back RJS! Any path ending in .rjs will be served from your Rails app. This allows you to import server rendered javascript.
698
646
 
699
- For example, if you set `REVISION=v1`, URL's will be appended with `?v1`: `/my/imported/file.js?v1`.
647
+ ## Resolution
700
648
 
701
- It is assumed that the `REVISION` env var will be unique between deploys. If it isn't, then assets will continue to be cached as the same version between deploys. I recommend you assign a version number or to use the Git commit hash of the deploy. Just make sure it is unique for each deploy.
649
+ Proscenium will serve files ending with any of these extension: `js,mjs,ts,css,jsx,tsx` from the following directories, and their sub-directories of your Rails application's root: `/app`, `/lib`, `/config`, `/node_modules`, `/vendor`.
702
650
 
703
- You can set the `cache_query_string` config option directly to define any query string you wish:
651
+ So a file at `/app/views/users/index.js` will be served from `https://localhost:3000/app/views/users/index.js`.
704
652
 
705
- ```ruby
706
- Rails.application.config.proscenium.cache_query_string = 'my-cache-busting-version-string'
707
- ```
653
+ You can continue to access any file in the `/public` directory as you normally would. Proscenium will not process files in the `/public` directory.
654
+
655
+ If requesting a file that exists in a root directory and the public directory, the file in the public directory will be served. For example, if you have a file at `/lib/foo.js` and `/public/lib/foo.js`, and you request `/lib/foo.js`, the file in the public directory (`/public/lib/foo.js`) will be served.
656
+
657
+ ## Aliases
708
658
 
709
- The cache is set with a `max-age` of 30 days. You can customise this with the `cache_max_age` config option:
659
+ You can define import aliases via the `config.proscenium.aliases` config option. This allows you to create shorter or more meaningful import paths.
710
660
 
711
661
  ```ruby
712
- Rails.application.config.proscenium.cache_max_age = 12.months.to_i
662
+ config.proscenium.aliases = {
663
+ "utils": "/lib/utils.js",
664
+ "components": "/app/components"
665
+ }
713
666
  ```
714
667
 
715
- ## rjs is back
668
+ You can then import using the alias:
716
669
 
717
- Proscenium brings back RJS! Any path ending in .rjs will be served from your Rails app. This allows you to import server rendered javascript.
670
+ ```js
671
+ import utils from "utils";
672
+ import Header from "components/header";
673
+ ```
718
674
 
719
- ## Resolution
675
+ ## Pre-compilation
720
676
 
721
- Proscenium will serve files ending with any of these extension: `js,mjs,ts,css,jsx,tsx` from the following directories, and their sub-directories of your Rails application's root: `/app`, `/lib`, `/config`, `/node_modules`, `/vendor`.
677
+ Proscenium is designed to bundle and minify your frontend code in real time, on demand, with no build step or pre-compilation needed. However, if you want to pre-compile your assets for production deployment, you can do so using the `assets:precompile` Rake task.
722
678
 
723
- So a file at `/app/views/users/index.js` will be served from `https://localhost:3000/app/views/users/index.js`.
679
+ ```bash
680
+ rails assets:precompile
681
+ ```
724
682
 
725
- You can continue to access any file in the `/public` directory as you normally would. Proscenium will not process files in the `/public` directory.
683
+ Be sure to specify a `Set` of paths which you want to pre-compile via the `config.proscenium.precompile` configuration option. Each path should be a glob pattern that matches the files which are your entry points. Don't include paths that are not entry points. For example:
726
684
 
727
- If requesting a file that exists in a root directory and the public directory, the file in the public directory will be served. For example, if you have a file at `/lib/foo.js` and `/public/lib/foo.js`, and you request `/lib/foo.js`, the file in the public directory (`/public/lib/foo.js`) will be served.
685
+ ```ruby
686
+ Rails.configuration.proscenium.precompile = Set[
687
+ "./app/components/**/*.js",
688
+ "./app/components/**/*.jsx",
689
+ "./app/views/**/*.js",
690
+ "./app/views/**/*.css",
691
+ "./app/views/**/*.module.css"
692
+ ]
693
+ ```
694
+
695
+ This will bundle, code split, tree shake, and compile all your JS, TS, JSX, TSX and CSS files and place them in the `public/assets` directory, ready to be served in production.
728
696
 
729
697
  ## Thanks
730
698
 
@@ -4,8 +4,6 @@ require 'ffi'
4
4
 
5
5
  module Proscenium
6
6
  class Builder
7
- class CompileError < StandardError; end
8
-
9
7
  ENVIRONMENTS = { development: 1, test: 2, production: 3 }.freeze
10
8
 
11
9
  class Result < FFI::Struct
@@ -14,6 +12,11 @@ module Proscenium
14
12
  :content_hash, :string
15
13
  end
16
14
 
15
+ class CompileResult < FFI::Struct
16
+ layout :success, :bool,
17
+ :messages, :string
18
+ end
19
+
17
20
  module Request
18
21
  extend FFI::Library
19
22
 
@@ -23,7 +26,6 @@ module Proscenium
23
26
 
24
27
  attach_function :build_to_string, [
25
28
  :string, # Path or entry point.
26
- :string, # cache_query_string.
27
29
  :pointer # Config as JSON.
28
30
  ], Result.by_value
29
31
 
@@ -32,41 +34,51 @@ module Proscenium
32
34
  :pointer # Config as JSON.
33
35
  ], Result.by_value
34
36
 
37
+ attach_function :compile, [
38
+ :pointer # Config as JSON.
39
+ ], CompileResult.by_value
40
+
35
41
  attach_function :reset_config, [], :void
36
42
  end
37
43
 
38
- class BuildError < StandardError
39
- attr_reader :error
44
+ class BuildError < Error
45
+ attr_reader :error, :path
40
46
 
41
- def initialize(error)
42
- @error = JSON.parse(error, strict: true).deep_transform_keys(&:underscore)
47
+ def initialize(path, error)
48
+ @path = path
49
+ @error = JSON.parse(error, strict: true)
43
50
 
44
- msg = @error['text']
45
- msg << ' - ' << @error['detail'] if @error['detail'].is_a?(String)
46
- if (location = @error['location'])
47
- msg << " at #{location['file']}:#{location['line']}:#{location['column']}"
51
+ msg = @error['Text']
52
+ msg << ' - ' << @error['Detail'] if @error['Detail'].is_a?(String)
53
+ if (location = @error['Location'])
54
+ msg << " at #{location['File']}:#{location['Line']}:#{location['Column']}"
48
55
  end
49
56
 
50
- super(msg)
57
+ super("Failed to build #{path} - #{msg}")
51
58
  end
52
59
  end
53
60
 
54
- class ResolveError < StandardError
55
- attr_reader :error_msg, :path
61
+ class ResolveError < Error
62
+ attr_reader :path
56
63
 
57
- def initialize(path, error_msg)
58
- super("Failed to resolve '#{path}' -- #{error_msg}")
64
+ def initialize(path, msg)
65
+ @path = path
66
+ super("Failed to resolve #{path} - #{msg}")
59
67
  end
60
68
  end
61
69
 
62
- def self.build_to_string(path, cache_query_string: '', root: nil)
63
- new(root:).build_to_string(path, cache_query_string:)
70
+ def self.build_to_string(path, root: nil)
71
+ new(root:).build_to_string(path)
64
72
  end
65
73
 
66
74
  def self.resolve(path, root: nil)
67
75
  new(root:).resolve(path)
68
76
  end
69
77
 
78
+ def self.compile(root: nil)
79
+ new(root:).compile
80
+ end
81
+
70
82
  # Intended for tests only.
71
83
  def self.reset_config!
72
84
  Request.reset_config
@@ -75,6 +87,7 @@ module Proscenium
75
87
  def initialize(root: nil)
76
88
  @request_config = FFI::MemoryPointer.from_string({
77
89
  RootPath: (root || Rails.root).to_s,
90
+ OutputDir: "public#{Proscenium.config.output_dir}",
78
91
  GemPath: gem_root,
79
92
  Environment: ENVIRONMENTS.fetch(Rails.env.to_sym, 2),
80
93
  EnvVars: env_vars,
@@ -82,16 +95,17 @@ module Proscenium
82
95
  RubyGems: Proscenium::BundledGems.paths,
83
96
  Bundle: Proscenium.config.bundle,
84
97
  Aliases: Proscenium.config.aliases,
85
- QueryString: Proscenium.config.cache_query_string.presence || '',
98
+ External: Proscenium.config.external,
99
+ Precompile: Proscenium.config.precompile,
86
100
  Debug: Proscenium.config.debug
87
101
  }.to_json)
88
102
  end
89
103
 
90
- def build_to_string(path, cache_query_string: '')
104
+ def build_to_string(path)
91
105
  ActiveSupport::Notifications.instrument('build.proscenium', identifier: path) do
92
- result = Request.build_to_string(path, cache_query_string, @request_config)
106
+ result = Request.build_to_string(path, @request_config)
93
107
 
94
- raise BuildError, result[:response] unless result[:success]
108
+ raise BuildError.new(path, result[:response]) unless result[:success]
95
109
 
96
110
  result
97
111
  end
@@ -107,6 +121,11 @@ module Proscenium
107
121
  end
108
122
  end
109
123
 
124
+ def compile
125
+ result = Request.compile(@request_config)
126
+ result[:success]
127
+ end
128
+
110
129
  private
111
130
 
112
131
  # Build the ENV variables as determined by `Proscenium.config.env_vars` and
@@ -7,7 +7,7 @@ module Proscenium::CssModule
7
7
  autoload :Transformer
8
8
  autoload :Rewriter
9
9
 
10
- class TransformError < StandardError
10
+ class TransformError < Proscenium::Error
11
11
  def initialize(name, additional_msg = nil)
12
12
  msg = "Failed to transform CSS module `#{name}`"
13
13
  msg << ' - ' << additional_msg if additional_msg
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Proscenium
4
- NotIncludedError = Class.new(StandardError)
4
+ NotIncludedError = Class.new(Error)
5
5
 
6
6
  module EnsureLoaded
7
7
  def self.included(child)
Binary file
@@ -25,6 +25,10 @@ struct Result {
25
25
  int success;
26
26
  char* response;
27
27
  char* contentHash;
28
+ };
29
+ struct CompileResult {
30
+ int success;
31
+ char* messages;
28
32
  };
29
33
 
30
34
  #line 1 "cgo-generated-wrapper"
@@ -90,7 +94,7 @@ extern void reset_config();
90
94
  // - path - The path to build relative to `root`.
91
95
  // - config
92
96
  //
93
- extern struct Result build_to_string(char* filePath, char* cacheQueryString, char* configJson);
97
+ extern struct Result build_to_string(char* filePath, char* configJson);
94
98
 
95
99
  // Resolve the given `path` relative to the `root`.
96
100
  //
@@ -99,6 +103,12 @@ extern struct Result build_to_string(char* filePath, char* cacheQueryString, cha
99
103
  //
100
104
  extern struct Result resolve(char* filePath, char* configJson);
101
105
 
106
+ // Compile assets using the given `config`.
107
+ //
108
+ // - config
109
+ //
110
+ extern struct CompileResult compile(char* configJson);
111
+
102
112
  #ifdef __cplusplus
103
113
  }
104
114
  #endif
@@ -11,18 +11,9 @@ module Proscenium
11
11
  end
12
12
  end
13
13
 
14
- # Overriden to allow regular use of javascript_include_tag and stylesheet_link_tag, while still
15
- # building with Proscenium. It's important to note that `include_assets` will not call this, as
16
- # those asset paths all begin with a slash, which the Rails asset helpers do not pass through to
17
- # here.
18
- #
19
- # If the given `path` is a bare path (does not start with `./` or `../`), then we use
20
- # Rails default conventions, and serve CSS from /app/assets/stylesheets and JS from
21
- # /app/javascript.
22
14
  def compute_asset_path(path, options = {})
23
15
  if %i[javascript stylesheet].include?(options[:type])
24
- path.prepend DEFAULT_RAILS_ASSET_PATHS[options[:type]] if !path.start_with?('./', '../')
25
- return path
16
+ return Proscenium::Manifest[path] || "/#{path}"
26
17
  end
27
18
 
28
19
  super
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium
4
+ module Manifest
5
+ mattr_accessor :manifest, default: {}
6
+ mattr_accessor :loaded, default: false
7
+
8
+ module_function
9
+
10
+ def loaded?
11
+ loaded
12
+ end
13
+
14
+ def load!
15
+ self.manifest = {}
16
+ self.loaded = false
17
+
18
+ if Proscenium.config.manifest_path.exist?
19
+ self.loaded = true
20
+
21
+ JSON.parse(Proscenium.config.manifest_path.read)['outputs'].each do |output_path, details|
22
+ next if !details.key?('entryPoint')
23
+
24
+ manifest[details['entryPoint']] = "/#{output_path.delete_prefix('public/')}"
25
+ end
26
+ end
27
+
28
+ manifest
29
+ end
30
+
31
+ def reset!
32
+ self.manifest = {}
33
+ self.loaded = false
34
+ end
35
+
36
+ def [](key)
37
+ loaded? ? manifest[key] : "/#{key}"
38
+ end
39
+ end
40
+ end
@@ -5,18 +5,6 @@ module Proscenium
5
5
  class Base
6
6
  include ActiveSupport::Benchmarkable
7
7
 
8
- # Error when the result of the build returns an error. For example, when esbuild returns
9
- # errors.
10
- class CompileError < StandardError
11
- attr_reader :detail, :file
12
-
13
- def initialize(args)
14
- @detail = args[:detail]
15
- @file = args[:file]
16
- super("Failed to build '#{args[:file]}' -- #{detail}")
17
- end
18
- end
19
-
20
8
  def self.attempt(request)
21
9
  new(request).renderable!&.attempt
22
10
  end
@@ -40,17 +28,6 @@ module Proscenium
40
28
  @path_to_build ||= @request.path[1..]
41
29
  end
42
30
 
43
- def cache_query_string
44
- @cache_query_string ||= begin
45
- params = @request.query_parameters
46
- if params.one? && params.first[0] != '' && params.first[1].nil?
47
- params.keys.first
48
- else
49
- Proscenium.config.cache_query_string
50
- end
51
- end.presence || ''
52
- end
53
-
54
31
  def sourcemap?
55
32
  @request.path.ends_with?('.map')
56
33
  end
@@ -97,10 +74,6 @@ module Proscenium
97
74
  response.content_type = content_type
98
75
  response.etag = result[:content_hash]
99
76
 
100
- if !cache_query_string.blank? && Proscenium.config.cache_max_age
101
- response.cache! Proscenium.config.cache_max_age
102
- end
103
-
104
77
  if @request.fresh?(response)
105
78
  response.status = 304
106
79
  response.body = []
@@ -0,0 +1,27 @@
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
+ return @app.call(env) unless request.path.match?(CHUNKS_PATH)
14
+
15
+ ActionDispatch::FileHandler.new(
16
+ Proscenium.config.output_path.to_s,
17
+ headers: {
18
+ 'X-Proscenium-Middleware' => 'chunks',
19
+ 'SourceMap' => "#{request.path}.map",
20
+ 'Cache-Control' => "public, max-age=#{100.years}, immutable",
21
+ 'ETag' => request.path.match(/-\$([a-z0-9]+)\$/i)[1]
22
+ }
23
+ ).attempt(request.env) || @app.call(env)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -3,26 +3,8 @@
3
3
  module Proscenium
4
4
  class Middleware
5
5
  class Esbuild < Base
6
- class CompileError < Base::CompileError
7
- def initialize(args)
8
- detail = args[:detail]
9
- detail = JSON.parse(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
19
- end
20
- end
21
-
22
6
  def attempt
23
- render_response Builder.build_to_string(path_to_build, cache_query_string:)
24
- rescue Builder::CompileError => e
25
- raise self.class::CompileError, { file: @request.fullpath, detail: e.message }, caller
7
+ render_response Builder.build_to_string(path_to_build)
26
8
  end
27
9
  end
28
10
  end
@@ -0,0 +1,28 @@
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
+ request.path_info = request.path.delete_prefix('/vendor')
17
+
18
+ ActionDispatch::FileHandler.new(
19
+ Rails.root.join('vendor').to_s,
20
+ headers: {
21
+ 'X-Proscenium-Middleware' => 'vendor',
22
+ 'Cache-Control' => "public, max-age=#{100.years}, immutable"
23
+ }
24
+ ).attempt(request.env) || @app.call(env)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -4,11 +4,13 @@ module Proscenium
4
4
  class Middleware
5
5
  extend ActiveSupport::Autoload
6
6
 
7
- class BuildError < StandardError; end
7
+ class BuildError < Error; end
8
8
 
9
9
  autoload :Base
10
10
  autoload :Esbuild
11
11
  autoload :RubyGems
12
+ autoload :Vendor
13
+ autoload :Chunks
12
14
  autoload :SilenceRequest
13
15
 
14
16
  def initialize(app)
@@ -20,19 +22,7 @@ module Proscenium
20
22
 
21
23
  return @app.call(env) if !request.get? && !request.head?
22
24
 
23
- # If this is a request for an asset chunk, we want to serve it with a very long
24
- # cache lifetime, since these are content-hashed and will never change.
25
- if request.path.match?(CHUNKS_PATH)
26
- ::ActionDispatch::FileHandler.new(
27
- Rails.public_path.join('assets').to_s,
28
- headers: {
29
- 'Cache-Control' => "public, max-age=#{100.years}, immutable",
30
- 'etag' => request.path.match(/-\$([a-z0-9]+)\$/i)[1]
31
- }
32
- ).attempt(env) || @app.call(env)
33
- else
34
- attempt(request) || @app.call(env)
35
- end
25
+ attempt(request) || @app.call(env)
36
26
  end
37
27
 
38
28
  private
@@ -40,7 +30,7 @@ module Proscenium
40
30
  def attempt(request)
41
31
  return unless (type = find_type(request))
42
32
 
43
- type.attempt request
33
+ type.attempt(request)
44
34
  end
45
35
 
46
36
  def find_type(request)
@@ -15,10 +15,10 @@ module Proscenium
15
15
  config.proscenium.side_load = true
16
16
  config.proscenium.code_splitting = true
17
17
  config.proscenium.ensure_loaded = :raise
18
- config.proscenium.cache_query_string = Rails.env.production? && ENV.fetch('REVISION', nil)
19
- config.proscenium.cache_max_age = 2_592_000 # 30 days
20
-
21
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
22
 
23
23
  # List of environment variable names that should be passed to the builder, which will then be
24
24
  # passed to esbuild's `Define` option. Being explicit about which environment variables are
@@ -29,7 +29,13 @@ module Proscenium
29
29
  'Proscenium::Builder::BuildError' => 'build_error'
30
30
  }
31
31
 
32
- config.after_initialize do |_app|
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
+
33
39
  if config.proscenium.logging
34
40
  require 'proscenium/log_subscriber'
35
41
  Proscenium::LogSubscriber.attach_to :proscenium
@@ -49,7 +55,18 @@ module Proscenium
49
55
  unless config.proscenium.logging
50
56
  app.middleware.insert_before Rails::Rack::Logger, Proscenium::Middleware::SilenceRequest
51
57
  end
52
- app.middleware.insert_before ActionDispatch::ActionableExceptions, Proscenium::Middleware
58
+
59
+ app.middleware.insert_after Rack::Sendfile, Proscenium::Middleware::Vendor
60
+ app.middleware.insert_after Rack::Sendfile, Proscenium::Middleware::Chunks
61
+
62
+ # Ensure the middleware is inserted as early as possible.
63
+ if app.config.consider_all_requests_local
64
+ app.middleware.insert_before ActionDispatch::ActionableExceptions, Proscenium::Middleware
65
+ elsif app.config.reloading_enabled?
66
+ app.middleware.insert_before ActionDispatch::Reloader, Proscenium::Middleware
67
+ else
68
+ app.middleware.insert_before ActionDispatch::Callbacks, Proscenium::Middleware
69
+ end
53
70
  end
54
71
 
55
72
  initializer 'proscenium.sideloading' do
@@ -65,5 +82,9 @@ module Proscenium
65
82
  ActionView::PartialRenderer.prepend Monkey::PartialRenderer
66
83
  end
67
84
  end
85
+
86
+ rake_tasks do
87
+ load 'proscenium/railties/assets.rake'
88
+ end
68
89
  end
69
90
  end
@@ -0,0 +1,18 @@
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
+ raise 'Assets pre-compilation failed!' unless Proscenium::Builder.compile
9
+
10
+ puts "\nAssets pre-compiled successfully."
11
+
12
+ if Rails.env.development?
13
+ puts "\nWarning: You are precompiling assets in development. Rails will not " \
14
+ 'serve any changed assets until you delete ' \
15
+ "public#{Rails.application.config.proscenium.output_dir}/.manifest.json"
16
+ end
17
+ end
18
+ end
@@ -40,10 +40,7 @@ module Proscenium
40
40
  opts[:preload_links_header] = false if fragments
41
41
  opts[:data] ||= {}
42
42
 
43
- if Proscenium.config.cache_query_string.present?
44
- path += "?#{Proscenium.config.cache_query_string}"
45
- end
46
- out << helpers.stylesheet_link_tag(path, extname: false, **opts)
43
+ out << helpers.stylesheet_link_tag(path.delete_prefix('/'), extname: false, **opts)
47
44
  end
48
45
 
49
46
  if fragments
@@ -71,10 +68,7 @@ module Proscenium
71
68
  opts = opts[:js].is_a?(Hash) ? opts[:js] : {}
72
69
  opts[:preload_links_header] = false if fragments
73
70
 
74
- if Proscenium.config.cache_query_string.present?
75
- path += "?#{Proscenium.config.cache_query_string}"
76
- end
77
- out << helpers.javascript_include_tag(path, extname: false, **opts)
71
+ out << helpers.javascript_include_tag(path.delete_prefix('/'), extname: false, **opts)
78
72
  end
79
73
 
80
74
  if fragments
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Proscenium
4
- VERSION = '0.21.6'
4
+ VERSION = '0.22.0'
5
5
  end
data/lib/proscenium.rb CHANGED
@@ -14,7 +14,8 @@ module Proscenium
14
14
 
15
15
  FILE_EXTENSIONS = ['js', 'mjs', 'ts', 'jsx', 'tsx', 'css', 'js.map', 'mjs.map', 'jsx.map',
16
16
  'ts.map', 'tsx.map', 'css.map'].freeze
17
- ALLOWED_DIRECTORIES = 'app,lib,config,vendor,node_modules'
17
+ ALLOWED_DIRECTORIES = 'app,lib,config,node_modules'
18
+ VENDOR_PATH_GLOB = '/vendor/**.{js,css}'
18
19
  APP_PATH_GLOB = "/{#{ALLOWED_DIRECTORIES}}/**.{#{FILE_EXTENSIONS.join(',')}}".freeze
19
20
  GEMS_PATH_GLOB = "/node_modules/@rubygems/**.{#{FILE_EXTENSIONS.join(',')}}".freeze
20
21
  CHUNKS_PATH = %r{^/_asset_chunks/}
@@ -26,6 +27,7 @@ module Proscenium
26
27
  autoload :Utils
27
28
  autoload :Monkey
28
29
  autoload :Middleware
30
+ autoload :Manifest
29
31
  autoload :EnsureLoaded
30
32
  autoload :SideLoad
31
33
  autoload :CssModule
@@ -44,7 +46,20 @@ module Proscenium
44
46
  end
45
47
  end
46
48
 
47
- class PathResolutionFailed < StandardError
49
+ class Error < StandardError; end
50
+
51
+ class MissingAssetError < Error
52
+ def initialize(path)
53
+ super
54
+ @path = path
55
+ end
56
+
57
+ def message
58
+ "The asset '#{@path}' was not found."
59
+ end
60
+ end
61
+
62
+ class PathResolutionFailed < Error
48
63
  def initialize(path)
49
64
  @path = path
50
65
  super
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: proscenium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.6
4
+ version: 0.22.0
5
5
  platform: arm64-darwin
6
6
  authors:
7
7
  - Joel Moss
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-10-24 00:00:00.000000000 Z
11
+ date: 2025-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -66,13 +66,17 @@ files:
66
66
  - lib/proscenium/helper.rb
67
67
  - lib/proscenium/importer.rb
68
68
  - lib/proscenium/log_subscriber.rb
69
+ - lib/proscenium/manifest.rb
69
70
  - lib/proscenium/middleware.rb
70
71
  - lib/proscenium/middleware/base.rb
72
+ - lib/proscenium/middleware/chunks.rb
71
73
  - lib/proscenium/middleware/esbuild.rb
72
74
  - lib/proscenium/middleware/ruby_gems.rb
73
75
  - lib/proscenium/middleware/silence_request.rb
76
+ - lib/proscenium/middleware/vendor.rb
74
77
  - lib/proscenium/monkey.rb
75
78
  - lib/proscenium/railtie.rb
79
+ - lib/proscenium/railties/assets.rake
76
80
  - lib/proscenium/react-manager/index.jsx
77
81
  - lib/proscenium/react-manager/react.js
78
82
  - lib/proscenium/react_componentable.rb