proscenium 0.9.1-x86_64-darwin → 0.10.0-x86_64-darwin

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5fac23eaa79ef18f9c4c9bf82d12ff14eca482e77aa8edacb4a56fe481b836eb
4
- data.tar.gz: 5c2480032ed37c8b6c71873a18095422ca3a83a618d1388cb6e8f8cdff72f922
3
+ metadata.gz: 9a7be514999fef4e971e665ca3fe60e2dfacec8182bae13ab36a072560892eae
4
+ data.tar.gz: ffbbcf6031273572ebb843c35966a6123b2f772b865839916b4b588441da5f14
5
5
  SHA512:
6
- metadata.gz: 3f7da4b2ccee543397ccee7a7056a113cf25e5dce643d0f73d3a7905c102b9361e0675d237b0bde8ce9b4853bdf83b647cf5f717e4245f61315c5fd1f2d23346
7
- data.tar.gz: dda1615fbbcd74a940a73f1b7fc586755ac6b7e659152e4ee017bbb0f3948db266ea444e622e878790b6601f9ef8f25acfbc578bbd5f1a61550dbb00cd415e4f
6
+ metadata.gz: 956bf71996aa9ed3fdcc674961915928f41db8e9cb5ced28643ab580e45e6624061efe63ae6d13477783d57503e5aa1d731ccb6c2d68ecfec60fd6eb3ef8664d
7
+ data.tar.gz: 781af921c0e2e0b96ab4260067cca59c41f31cdef7d2e596779fa2e06e609b446869db6b7435f131c6b2a926b184fa687a30e0415e00b9f86d46e3a0fe621034
data/README.md CHANGED
@@ -4,28 +4,64 @@ Proscenium treats your client-side code as first class citizens of your Rails ap
4
4
  "fast by default" internet. It bundles your JS, JSX and CSS in real time, on demand, and with zero
5
5
  configuration.
6
6
 
7
- - Fast real-time bundling, tree-shaking and minification.
8
- - Real time bundling of Javascript (.js,.jsx), Typescript (.ts,.tsx) and CSS (.css).
9
- - NO JavaScript runtime - just the browser!
7
+ - Fast real-time bundling, tree-shaking and minification of Javascript (.js,.jsx), Typescript (.ts,.tsx) and CSS (.css).
8
+ - NO JavaScript runtime needed - just the browser!
10
9
  - NO build step or pre-compilation.
11
10
  - NO additional process or server - Just run Rails!
12
11
  - Deep integration with Rails.
13
12
  - Zero configuration.
14
13
  - Serve assets from anywhere within your Rails root (/app, /config, /lib, etc.).
15
14
  - Automatically side load JS/TS/CSS for your layouts and views.
16
- - Import from NPM, URLs, and locally.
15
+ - ESM importing from NPM, URLs, and locally.
17
16
  - Server-side import map support.
18
- - CSS Modules.
19
- - CSS mixins.
17
+ - CSS Modules & mixins.
20
18
  - Source maps.
21
- - Phlex and ViewComponent integration.
19
+
20
+ ## Table of Contents
21
+
22
+ - [Getting Started](#getting-started)
23
+ - [Installation](#installation)
24
+ - [Client-Side Code Anywhere](#client-side-code-anywhere)
25
+ - [Side Loading](#side-loading)
26
+ - [Importing](#importing-assets)
27
+ - [URL Imports](#url-imports)
28
+ - [Local Imports](#local-imports)
29
+ - [Import Maps](#import-maps)
30
+ - [Source Maps](#source-maps)
31
+ - [SVG](#svg)
32
+ - [Environment Variables](#environment-variables)
33
+ - [i18n](#i18n)
34
+ - [JavaScript](#javascript)
35
+ - [Tree Shaking](#tree-shaking)
36
+ - [Code Splitting](#code-splitting)
37
+ - [JavaScript Caveats](#javascript-caveats)
38
+ - [CSS](#css)
39
+ - [Importing CSS from JavaScript](#importing-css-from-javascript)
40
+ - [CSS Modules](#css-modules)
41
+ - [CSS Mixins](#css-mixins)
42
+ - [CSS Caveats](#css-caveats)
43
+ - [Typescript](#typescript)
44
+ - [Typescript Caveats](#typescript-caveats)
45
+ - [JSX](#jsx)
46
+ - [JSON](#json)
47
+ - [Phlex Support](#phlex-support)
48
+ - [ViewComponent Support](#viewcomponent-support)
49
+ - [Cache Busting](#cache-busting)
50
+ - [rjs is back!](#rjs-is-back)
51
+ - [Included Paths](#included-paths)
52
+ - [Thanks](#thanks)
53
+ - [Development](#development)
22
54
 
23
55
  ## Getting Started
24
56
 
25
57
  Getting started obviously depends on whether you are adding Proscenium to an existing Rails app, or creating a new Rails app. So please choose the appropriate guide below:
26
58
 
27
59
  - [Getting Started with a new Rails app](https://github.com/joelmoss/proscenium/blob/master/docs/guides/new_rails_app.md)
28
- - Getting Started with an existing Rails app *[Coming soon]*
60
+ - Getting Started with an existing Rails app
61
+ - [Migrate from Sprockets](docs/guides/migrate_from_sprockets.md)
62
+ - Migrate from Propshaft *[Coming soon]*
63
+ - Migrate from Webpacker *[Coming soon]*
64
+ - [Render a React component with Proscenium](docs/guides/basic_react.md)
29
65
 
30
66
  ## Installation
31
67
 
@@ -53,26 +89,83 @@ Using the examples above...
53
89
  - `app/components/menu_component.jsx` => `https://yourapp.com/app/components/menu_component.jsx`
54
90
  - `config/properties.css` => `https://yourapp.com/config/properties.css`
55
91
 
56
- ### Include Paths
92
+ ## Side Loading
57
93
 
58
- By default, Proscenium will serve files ending with any of these extension: `js,mjs,ts,css,jsx,tsx`, and only from `app/assets`, `config`, `app/views`, `lib` and `node_modules` directories.
94
+ > Prior to **0.10.0**, only assets with the extension `.js`, `.ts` and `.css` were side loaded. From 0.10.0, all assets are side loaded, including `.jsx`, and `.tsx`. Also partials were not side loaded prior to 0.10.0.
59
95
 
60
- However, you can customise these paths with the `include_path` config option...
96
+ Proscenium is best experienced when you side load your assets.
61
97
 
62
- ```ruby
63
- Rails.application.config.proscenium.include_paths << 'app/components'
98
+ ### The Problem
99
+
100
+ With Rails you would typically declaratively load your JavaScript and CSS assets using the `javascript_include_tag` and `stylesheet_link_tag` helpers.
101
+
102
+ For example, you may have top-level "application" CSS located in a file at `/app/assets/application.css`. Likewise, you may have some global JavaScript located in a file at `/app/assets/application.js`.
103
+
104
+ You would include those two files in your application layout, something like this:
105
+
106
+ ```erb
107
+ <%# /app/views/layouts/application.html.erb %>
108
+
109
+ <!DOCTYPE html>
110
+ <html>
111
+ <head>
112
+ <title>Hello World</title>
113
+ <%= stylesheet_link_tag 'application' %> <!-- << Your app CSS -->
114
+ </head>
115
+ <body>
116
+ <%= yield %>
117
+ <%= javascript_include_tag 'application' %> <!-- << Your app JS -->
118
+ </body>
119
+ </html>
64
120
  ```
65
121
 
66
- ## Side Loading
122
+ Now, you may have some CSS and JavaScript that is only required by a specific view and partial, so you would load that in your view, something like this:
123
+
124
+ ```erb
125
+ <%# /app/views/users/index.html.erb %>
126
+
127
+ <%= stylesheet_link_tag 'users' %>
128
+ <%= javascript_include_tag 'users' %>
129
+
130
+ <%# needed by the `users/_user.html.erb` partial %>
131
+ <%= javascript_include_tag '_user' %>
132
+
133
+ <% render @users %>
134
+ ```
135
+
136
+ The main problem is that you have to keep track of all these assets, and make sure each is loaded by all the views that require them, but also avoid loading them when not needed. This can be a real pain, especially when you have a lot of views.
137
+
138
+ ### The Solution
67
139
 
68
- Proscenium has built in support for automatically side loading JS, TS and CSS with your views and
69
- layouts.
140
+ When side loading your JavaScript, Typescript and CSS with Proscenium, they are automatically included alongside your views, partials, layouts, and components, and only when needed.
70
141
 
71
- Just create a JS and/or CSS file with the same name as any view or layout, and make sure your
72
- layouts include `<%= side_load_stylesheets %>` and `<%= side_load_javascripts %>`. Something like
73
- this:
142
+ Side loading works by looking for a JS/TS/CSS file with the same name as your view, partial, layout or component. For example, if you have a view at `app/views/users/index.html.erb`, then Proscenium will look for a JS/TS/CSS file at `app/views/users/index.js`, `app/views/users/index.ts` or `app/views/users/index.css`. If it finds one, it will include it in the HTML for that view.
74
143
 
75
- ```html
144
+ JSX is also supported for JavaScript and Typescript. Simply use the `.jsx` or `.tsx` extension instead of `.js` or `.ts`.
145
+
146
+ ### Usage
147
+
148
+ Simply create a JS and/or CSS file with the same name as any view, partial or layout.
149
+
150
+ Let's continue with our problem example above, where we have the following assets
151
+
152
+ - `/app/assets/application.css`
153
+ - `/app/assets/application.js`
154
+ - `/app/assets/users.css`
155
+ - `/app/assets/users.js`
156
+ - `/app/assets/user.js`
157
+
158
+ Your application layout is at `/app/views/layouts/application.hml.erb`, and the view that needs the users assets is at `/app/views/users/index.html.erb`, so move your assets JS and CSS alongside them:
159
+
160
+ - `/app/views/layouts/application.css`
161
+ - `/app/views/layouts/application.js`
162
+ - `/app/views/users/index.css`
163
+ - `/app/views/users/index.js`
164
+ - `/app/views/users/_user.js` (partial)
165
+
166
+ Now, in your layout and view, replace the `javascript_include_tag` and `stylesheet_link_tag` helpers with the `side_load_stylesheets` and `side_load_javascripts` helpers from Proscenium. Something like this:
167
+
168
+ ```erb
76
169
  <!DOCTYPE html>
77
170
  <html>
78
171
  <head>
@@ -81,20 +174,22 @@ this:
81
174
  </head>
82
175
  <body>
83
176
  <%= yield %>
84
- <%= side_load_javascripts defer: true, type: 'module' %>
177
+ <%= side_load_javascripts type: 'module', defer: true %>
85
178
  </body>
86
179
  </html>
87
180
  ```
88
181
 
89
- On each page request, Proscenium will check if your layout and view has a JS/TS/CSS file of the same
90
- name, and include them into your layout HTML. Partials are not side loaded.
182
+ > NOTE that Proscenium is desiged to work with modern JavaAscript, and assumes [ESModules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) are used everywhere. This is why the `type` attribute is set to `module` in the example above. If you are not using ESModules, then you can omit the `type` attribute.
183
+
184
+ On each page request, Proscenium will check if your views, layouts and partials have a JS/TS/CSS file of the same name, and then include them wherever your placed the `side_load_stylesheets` and `side_load_javascripts` helpers.
91
185
 
92
- Side loading is enabled by default, but you can disable it by setting `config.proscenium.side_load`
93
- to `false`.
186
+ Now you never have to remember to include your assets again. Just create them alongside your views, partials and layouts, and Proscenium will take care of the rest.
94
187
 
95
- ## Importing
188
+ Side loading is enabled by default, but you can disable it by setting `config.proscenium.side_load` to `false` in your `/config/application.rb`.
96
189
 
97
- Proscenium supports importing JS, JSX, TS and CSS from NPM, by URL, your local app, and even from Ruby Gems.
190
+ ## Importing Assets
191
+
192
+ Proscenium supports importing JS, JSX, TS, TSX, CSS and SVG from NPM, by URL, your local app, and even from Ruby Gems.
98
193
 
99
194
  Imported files are bundled together in real time. So no build step or pre-compilation is needed.
100
195
 
@@ -134,9 +229,13 @@ import utils from '/lib/utils'
134
229
  import constants from './constants'
135
230
  ```
136
231
 
137
- ## Import Map [WIP]
232
+ ## Import Maps
233
+
234
+ > **[WIP]**
138
235
 
139
- [Import map](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. If you are not familiar with import maps, think of it as a way to define aliases.
236
+ [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.
237
+
238
+ If you are not familiar with import maps, think of them as a way to define aliases.
140
239
 
141
240
  Just create `config/import_map.json` and specify the imports you want to use. For example:
142
241
 
@@ -166,7 +265,7 @@ and for CSS...
166
265
  @import '@radix-ui/colors/blue.css';
167
266
  ```
168
267
 
169
- 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.
268
+ 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.
170
269
 
171
270
  ```js
172
271
  env => ({
@@ -176,21 +275,89 @@ env => ({
176
275
  })
177
276
  ```
178
277
 
179
- ## Importing SVG from JS(X) and TS(X)
278
+ ## Source Maps
279
+
280
+ 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.
281
+
282
+ Source map output is supported for both JavaScript and CSS. Each file is appended with the link to the source map. For example:
283
+
284
+ ```js
285
+ //# sourceMappingURL=/app/views/layouts/application.js.map
286
+ ```
287
+
288
+ Your browsers dev tools should pick this up and automatically load the source map when and where needed.
180
289
 
181
- Importing SVG from JS(X) will bundle the SVG source code. Additionally, if importing from JSX or TSX, the SVG source code will be rendered as a JSX/TSX component.
290
+ ## SVG
291
+
292
+ You can import SVG from JS(X), which will bundle the SVG source code. Additionally, if importing from JSX or TSX, the SVG source code will be rendered as a JSX/TSX component.
182
293
 
183
294
  ## Environment Variables
184
295
 
185
- Import any environment variables into your JS(X) code.
296
+ > Available in `>=0.10.0`
297
+
298
+ You can define and access any environment variable from your JavaScript and Typescript under the `proscenium.env` namespace.
299
+
300
+ For performance and security reasons you must declare the environment variable names that you wish to expose in your `config/application.rb` file.
301
+
302
+ ```ruby
303
+ config.proscenium.env_vars = Set['API_KEY', 'SOME_SECRET_VARIABLE']
304
+ config.proscenium.env_vars << 'ANOTHER_API_KEY'
305
+ ```
306
+
307
+ This assumes that the environment variable of the same name has already been defined. If not, you will need to define it yourself either in your code using Ruby's `ENV` object, or in your shell.
308
+
309
+ These declared environment variables will be replaced with constant expressions, allowing you to use this like this:
186
310
 
187
311
  ```js
188
- import RAILS_ENV from '@proscenium/env/RAILS_ENV'
312
+ console.log(proscenium.env.RAILS_ENV) // console.log("development")
313
+ console.log(proscenium.env.RAILS_ENV === 'development') // console.log(true)
189
314
  ```
190
315
 
191
- You can only access environment variables that are explicitly named. It will export `undefined` if the env variable does not exist.
316
+ The `RAILS_ENV` and `NODE_ENV` environment variables will always automatically be declared for you.
317
+
318
+ In addition to this, Proscenium also provides a `process.env.NODE_ENV` variable, which is set to the same value as `proscenium.env.RAILS_ENV`. It is provided to support the community's existing tooling, which often relies on this variable.
192
319
 
193
- ## Importing i18n
320
+ Environment variables are particularly powerful in aiding [tree shaking](#tree-shaking).
321
+
322
+ ```js
323
+ function start() {
324
+ console.log("start")
325
+ }
326
+ function doSomethingDangerous() {
327
+ console.log("resetDatabase")
328
+ }
329
+
330
+ proscenium.env.RAILS_ENV === "development" && doSomethingDangerous()
331
+
332
+ start()
333
+ ```
334
+
335
+ In development the above code will be transformed into the following code, discarding the definition, and call to`doSomethingDangerous()`.
336
+
337
+ ```js
338
+ function start() {
339
+ console.log("start")
340
+ }
341
+ start()
342
+ ```
343
+
344
+ Please note that for security reasons environment variables are not replaced in URL imports.
345
+
346
+ An undefined environment variable will be replaced with `undefined`.
347
+
348
+ ```js
349
+ console.log(proscenium.env.UNKNOWN) // console.log((void 0).UNKNOWN)
350
+ ```
351
+
352
+ This means that code that relies on this will not be tree shaken. You can work around this by using the [optional chaining operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining):
353
+
354
+ ```js
355
+ if (typeof proscenium.env?.UNKNOWN !== "undefined") {
356
+ // do something if UNKNOWN is defined
357
+ }
358
+ ```
359
+
360
+ ## i18n
194
361
 
195
362
  Basic support is provided for importing your Rails locale files from `config/locales/*.yml`, exporting them as JSON.
196
363
 
@@ -226,6 +393,51 @@ function one() {
226
393
  one();
227
394
  ```
228
395
 
396
+ ### Code Splitting
397
+
398
+ > Available in `>=0.10.0`.
399
+
400
+ > #### *Experimental!* 🧪
401
+ >
402
+ > Code splitting is currently experimentally and limited to side loaded code. It is disabled by default. You can enable code splitting by setting the `code_splitting` configuration option to `true` in your application's `/config/application.rb`:
403
+ >
404
+ > ```ruby
405
+ > config.proscenium.code_splitting = true
406
+ > ```
407
+
408
+ [Side loaded](#side-loading) assets are automatically code split. This means that if you have a file that is imported and used imported several times, and by different files, it will be split off into a separate file.
409
+
410
+ As an example:
411
+
412
+ ```js
413
+ // /lib/son.js
414
+ import father from "./father";
415
+
416
+ father() + " and Son";
417
+ ```
418
+
419
+ ```js
420
+ // /lib/daughter.js
421
+ import father from "./father";
422
+
423
+ father() + " and Daughter";
424
+ ```
425
+
426
+ ```js
427
+ // /lib/father.js
428
+ export default () => "Father";
429
+ ```
430
+
431
+ Both `son.js` and `daughter.js` import `father.js`, so both son and daughter would usually include a copy of father, resulting in duplicated code and larger bundle sizes.
432
+
433
+ If these files are side loaded, then `father.js` will be split off into a separate file or chunk, and only downloaded once.
434
+
435
+ - Code shared between multiple entry points is split off into a separate shared file that both entry points import. That way if the user first browses to one page and then to another page, they don't have to download all of the JavaScript for the second page from scratch if the shared part has already been downloaded and cached by their browser.
436
+
437
+ - Code referenced through an asynchronous `import()` expression will be split off into a separate file and only loaded when that expression is evaluated. This allows you to improve the initial download time of your app by only downloading the code you need at startup, and then lazily downloading additional code if needed later.
438
+
439
+ - Without code splitting, an import() expression becomes `Promise.resolve().then(() => require())` instead. This still preserves the asynchronous semantics of the expression but it means the imported code is included in the same bundle instead of being split off into a separate file.
440
+
229
441
  ### JavaScript Caveats
230
442
 
231
443
  There are a few important caveats as far as JavaScript is concerned. These are [detailed on the esbuild site](https://esbuild.github.io/content-types/#javascript-caveats).
@@ -238,7 +450,7 @@ Note that by default, Proscenium's output will take advantage of all modern CSS
238
450
 
239
451
  The new CSS nesting syntax is supported, and transformed into non-nested CSS for older browsers.
240
452
 
241
- ### Importing from JavaScript
453
+ ### Importing CSS from JavaScript
242
454
 
243
455
  You can also import CSS from JavaScript. When you do this, Proscenium will automatically append each stylesheet to the document's head as a `<link>` element.
244
456
 
@@ -377,13 +589,15 @@ console.log(version)
377
589
 
378
590
  ## Phlex Support
379
591
 
380
- *docs needed*
592
+ > *docs needed*
381
593
 
382
594
  ## ViewComponent Support
383
595
 
384
- *docs needed*
596
+ > *docs needed*
597
+
598
+ ## Cache Busting
385
599
 
386
- ## Cache Busting [*COMING SOON*]
600
+ > *COMING SOON*
387
601
 
388
602
  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.
389
603
 
@@ -411,9 +625,19 @@ Proscenium brings back RJS! Any path ending in .rjs will be served from your Rai
411
625
 
412
626
  *docs needed*
413
627
 
414
- ## Thanks 🙏
628
+ ## Included Paths
629
+
630
+ By default, Proscenium will serve files ending with any of these extension: `js,mjs,ts,css,jsx,tsx`, and only from `app/assets`, `config`, `app/views`, `lib` and `node_modules` directories.
631
+
632
+ However, you can customise these paths with the `include_path` config option...
633
+
634
+ ```ruby
635
+ Rails.application.config.proscenium.include_paths << 'app/components'
636
+ ```
637
+
638
+ ## Thanks
415
639
 
416
- HUGE thanks go to [Evan Wallace](https://github.com/evanw) and his amazing [esbuild](https://esbuild.github.io/) project. Proscenium would not be possible without it, and it is esbuild that makes this so fast and efficient.
640
+ HUGE thanks 🙏 go to [Evan Wallace](https://github.com/evanw) and his amazing [esbuild](https://esbuild.github.io/) project. Proscenium would not be possible without it, and it is esbuild that makes this so fast and efficient.
417
641
 
418
642
  Because Proscenium uses esbuild extensively, some of these docs are taken directly from the esbuild docs, with links back to the [esbuild site](https://esbuild.github.io/) where appropriate.
419
643
 
@@ -4,7 +4,9 @@ require 'ffi'
4
4
  require 'oj'
5
5
 
6
6
  module Proscenium
7
- class Esbuild::Golib
7
+ class Builder
8
+ class CompileError < StandardError; end
9
+
8
10
  class Result < FFI::Struct
9
11
  layout :success, :bool,
10
12
  :response, :string
@@ -12,24 +14,30 @@ module Proscenium
12
14
 
13
15
  module Request
14
16
  extend FFI::Library
15
- ffi_lib Pathname.new(__dir__).join('../ext/proscenium').to_s
17
+ ffi_lib Pathname.new(__dir__).join('ext/proscenium').to_s
16
18
 
17
19
  enum :environment, [:development, 1, :test, :production]
18
20
 
19
21
  attach_function :build, [
20
- :string, # path or entry point
21
- :string, # root
22
+ :string, # path or entry point. multiple can be given by separating with a semi-colon
22
23
  :string, # base URL of the Rails app. eg. https://example.com
23
- :environment, # Rails environment as a Symbol
24
24
  :string, # path to import map, relative to root
25
+ :string, # ENV variables as a JSON string
26
+
27
+ # Config
28
+ :string, # root
29
+ :environment, # Rails environment as a Symbol
30
+ :bool, # code splitting enabled?
25
31
  :bool # debugging enabled?
26
32
  ], Result.by_value
27
33
 
28
34
  attach_function :resolve, [
29
35
  :string, # path or entry point
36
+ :string, # path to import map, relative to root
37
+
38
+ # Config
30
39
  :string, # root
31
- :environment, # Rails environment as a Symbol
32
- :string # path to import map, relative to root
40
+ :environment # Rails environment as a Symbol
33
41
  ], Result.by_value
34
42
  end
35
43
 
@@ -51,29 +59,33 @@ module Proscenium
51
59
  end
52
60
  end
53
61
 
54
- def initialize(root: nil, base_url: nil)
55
- @root = root || Rails.root
56
- @base_url = base_url
62
+ def self.build(path, root: nil, base_url: nil)
63
+ new(root: root, base_url: base_url).build(path)
57
64
  end
58
65
 
59
- def self.resolve(path)
60
- new.resolve(path)
66
+ def self.resolve(path, root: nil)
67
+ new(root: root).resolve(path)
61
68
  end
62
69
 
63
- def self.build(path)
64
- new.build(path)
70
+ def initialize(root: nil, base_url: nil)
71
+ @root = root || Rails.root
72
+ @base_url = base_url
65
73
  end
66
74
 
67
75
  def build(path)
68
- result = Request.build(path, @root.to_s, @base_url, Rails.env.to_sym, import_map,
69
- Rails.env.development?)
76
+ result = Request.build(path, @base_url, import_map, env_vars.to_json,
77
+ @root.to_s,
78
+ Rails.env.to_sym,
79
+ Proscenium.config.code_splitting,
80
+ Proscenium.config.debug)
81
+
70
82
  raise BuildError.new(path, result[:response]) unless result[:success]
71
83
 
72
84
  result[:response]
73
85
  end
74
86
 
75
87
  def resolve(path)
76
- result = Request.resolve(path, @root.to_s, Rails.env.to_sym, import_map)
88
+ result = Request.resolve(path, import_map, @root.to_s, Rails.env.to_sym)
77
89
  raise ResolveError.new(path, result[:response]) unless result[:success]
78
90
 
79
91
  result[:response]
@@ -81,12 +93,25 @@ module Proscenium
81
93
 
82
94
  private
83
95
 
96
+ # Build the ENV variables as determined by `Proscenium.config.env_vars` and
97
+ # `Proscenium::DEFAULT_ENV_VARS` to pass to esbuild.
98
+ def env_vars
99
+ ENV['NODE_ENV'] = ENV.fetch('RAILS_ENV', nil)
100
+ ENV.slice(*Proscenium.config.env_vars + Proscenium::DEFAULT_ENV_VARS)
101
+ end
102
+
103
+ def cache_query_string
104
+ q = Proscenium.config.cache_query_string
105
+ q ? "--cache-query-string #{q}" : nil
106
+ end
107
+
84
108
  def import_map
85
109
  return unless (path = Rails.root&.join('config'))
86
110
 
87
111
  if (json = path.join('import_map.json')).exist?
88
112
  return json.relative_path_from(@root).to_s
89
113
  end
114
+
90
115
  if (js = path.join('import_map.js')).exist?
91
116
  return js.relative_path_from(@root).to_s
92
117
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Proscenium::Componentable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ # @return [Hash] the props to pass to the React component.
8
+ attr_writer :props
9
+
10
+ # The HTML tag to use as the wrapping element for the component. You can reassign this in your
11
+ # component class to use a different tag:
12
+ #
13
+ # class MyComponent < Proscenium::ViewComponent::ReactComponent
14
+ # self.root_tag = :span
15
+ # end
16
+ #
17
+ # @return [Symbol]
18
+ class_attribute :root_tag, instance_predicate: false, default: :div
19
+
20
+ # Should the template block be forwarded as children to the React component?
21
+ #
22
+ # @return [Boolean]
23
+ class_attribute :forward_children, default: false
24
+ end
25
+
26
+ # @param props: [Hash]
27
+ def initialize(props: {})
28
+ @props = props
29
+
30
+ super()
31
+ end
32
+
33
+ def virtual_path
34
+ Proscenium::Utils.resolve_path path.sub_ext('.jsx').to_s
35
+ end
36
+
37
+ private
38
+
39
+ def data_attributes
40
+ d = {
41
+ proscenium_component_path: virtual_path,
42
+ proscenium_component_props: prepared_props
43
+ }
44
+
45
+ d[:proscenium_component_forward_children] = true if forward_children?
46
+
47
+ d
48
+ end
49
+
50
+ def props
51
+ @props ||= {}
52
+ end
53
+
54
+ def prepared_props
55
+ props.deep_transform_keys do |term|
56
+ # This ensures that the first letter after a slash is not capitalized.
57
+ string = term.to_s.split('/').map { |str| str.camelize :lower }.join('/')
58
+
59
+ # Reverses the effect of ActiveSupport::Inflector.camelize converting slashes into `::`.
60
+ string.gsub '::', '/'
61
+ end.to_json
62
+ end
63
+ end
Binary file
@@ -85,23 +85,30 @@ extern "C" {
85
85
 
86
86
  // Build the given `path` in the `root`.
87
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
- // - debug
88
+ // BuildOptions
89
+ // - path - The path to build relative to `root`. Multiple paths can be given by separating them
90
+ // with a semi-colon.
91
+ // - baseUrl - base URL of the Rails app. eg. https://example.com
92
+ // - importMap - Path to the import map relative to `root`.
93
+ // - envVars - JSON string of environment variables.
94
+ // Config:
95
+ // - root - The working directory.
96
+ // - env - The environment (1 = development, 2 = test, 3 = production)
97
+ // - codeSpitting?
98
+ // - debug?
94
99
  //
95
- extern struct Result build(char* filepath, char* root, char* baseUrl, unsigned int env, char* importMap, GoUint8 debug);
100
+ extern struct Result build(char* filepath, char* baseUrl, char* importMap, char* envVars, char* root, unsigned int env, GoUint8 codeSplitting, GoUint8 debug);
96
101
 
97
102
  // Resolve the given `path` relative to the `root`.
98
103
  //
99
- // - path - The path to build relative to `root`.
100
- // - root - The working directory.
101
- // - env - The environment (1 = development, 2 = test, 3 = production)
102
- // - importMap - Path to the import map relative to `root`.
104
+ // ResolveOptions
105
+ // - path - The path to build relative to `root`.
106
+ // - importMap - Path to the import map relative to `root`.
107
+ // Config
108
+ // - root - The working directory.
109
+ // - env - The environment (1 = development, 2 = test, 3 = production)
103
110
  //
104
- extern struct Result resolve(char* path, char* root, unsigned int env, char* importMap);
111
+ extern struct Result resolve(char* path, char* importMap, char* root, unsigned int env);
105
112
 
106
113
  #ifdef __cplusplus
107
114
  }
@@ -0,0 +1,83 @@
1
+ const controllerAttribute = "data-controller";
2
+ const controllerFilenameExtension = ".js";
3
+
4
+ export function lazyLoadControllersFrom(
5
+ under,
6
+ application,
7
+ element = document
8
+ ) {
9
+ lazyLoadExistingControllers(under, application, element);
10
+ lazyLoadNewControllers(under, application, element);
11
+ }
12
+
13
+ function lazyLoadExistingControllers(under, application, element) {
14
+ queryControllerNamesWithin(element).forEach((controllerName) =>
15
+ loadController(controllerName, under, application)
16
+ );
17
+ }
18
+
19
+ function lazyLoadNewControllers(under, application, element) {
20
+ new MutationObserver((mutationsList) => {
21
+ for (const { attributeName, target, type } of mutationsList) {
22
+ switch (type) {
23
+ case "attributes": {
24
+ if (
25
+ attributeName == controllerAttribute &&
26
+ target.getAttribute(controllerAttribute)
27
+ ) {
28
+ extractControllerNamesFrom(target).forEach((controllerName) =>
29
+ loadController(controllerName, under, application)
30
+ );
31
+ }
32
+ }
33
+
34
+ case "childList": {
35
+ lazyLoadExistingControllers(under, application, target);
36
+ }
37
+ }
38
+ }
39
+ }).observe(element, {
40
+ attributeFilter: [controllerAttribute],
41
+ subtree: true,
42
+ childList: true,
43
+ });
44
+ }
45
+
46
+ function queryControllerNamesWithin(element) {
47
+ return Array.from(element.querySelectorAll(`[${controllerAttribute}]`))
48
+ .map(extractControllerNamesFrom)
49
+ .flat();
50
+ }
51
+
52
+ function extractControllerNamesFrom(element) {
53
+ return element
54
+ .getAttribute(controllerAttribute)
55
+ .split(/\s+/)
56
+ .filter((content) => content.length);
57
+ }
58
+
59
+ function loadController(name, under, application) {
60
+ if (canRegisterController(name, application)) {
61
+ import(controllerFilename(name, under))
62
+ .then((module) => registerController(name, module, application))
63
+ .catch((error) =>
64
+ console.error(`Failed to autoload controller: ${name}`, error)
65
+ );
66
+ }
67
+ }
68
+
69
+ function controllerFilename(name, under) {
70
+ return `${under}/${name
71
+ .replace(/--/g, "/")
72
+ .replace(/-/g, "_")}_controller${controllerFilenameExtension}`;
73
+ }
74
+
75
+ function registerController(name, module, application) {
76
+ if (canRegisterController(name, application)) {
77
+ application.register(name, module.default);
78
+ }
79
+ }
80
+
81
+ function canRegisterController(name, application) {
82
+ return !application.router.modulesByIdentifier.has(name);
83
+ }
@@ -12,12 +12,11 @@ module Proscenium
12
12
 
13
13
  def build(event)
14
14
  path = event.payload[:identifier]
15
- path = path.start_with?(/https?%3A%2F%2F/) ? CGI.unescape(path) : path
15
+ path = CGI.unescape(path) if path.start_with?(/https?%3A%2F%2F/)
16
16
 
17
17
  info do
18
18
  message = +"[Proscenium] Building #{path}"
19
19
  message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
20
- message << "\n" if defined?(Rails.env) && Rails.env.development?
21
20
  end
22
21
  end
23
22
  end
@@ -51,7 +51,7 @@ module Proscenium
51
51
  end
52
52
 
53
53
  def file_readable?
54
- return unless (path = clean_path(sourcemap? ? real_path[0...-4] : real_path))
54
+ return false unless (path = clean_path(sourcemap? ? real_path[0...-4] : real_path))
55
55
 
56
56
  file_stat = File.stat(Pathname(root).join(path.delete_prefix('/').b).to_s)
57
57
  rescue SystemCallError
@@ -21,10 +21,10 @@ module Proscenium
21
21
 
22
22
  def attempt
23
23
  ActiveSupport::Notifications.instrument('build.proscenium', identifier: path_to_build) do
24
- render_response Proscenium::Esbuild.build(path_to_build, root: root,
24
+ render_response Proscenium::Builder.build(path_to_build, root: root,
25
25
  base_url: @request.base_url)
26
26
  end
27
- rescue Proscenium::Esbuild::CompileError => e
27
+ rescue Proscenium::Builder::CompileError => e
28
28
  raise self.class::CompileError, { file: @request.fullpath, detail: e.message }, caller
29
29
  end
30
30
  end
@@ -13,12 +13,17 @@ module Proscenium
13
13
 
14
14
  def initialize(app)
15
15
  @app = app
16
+
17
+ chunks_path = Rails.public_path.join('assets').to_s
18
+ headers = Rails.application.config.public_file_server.headers || {}
19
+ @chunk_handler = ::ActionDispatch::FileHandler.new(chunks_path, headers: headers)
16
20
  end
17
21
 
18
22
  def call(env)
19
23
  request = Rack::Request.new(env)
20
24
 
21
25
  return @app.call(env) if !request.get? && !request.head?
26
+ return @chunk_handler.attempt(request.env) if request.path.match?(%r{^/_asset_chunks/})
22
27
 
23
28
  attempt(request) || @app.call(env)
24
29
  end
@@ -5,23 +5,5 @@ module Proscenium::Phlex::ComponentConcerns
5
5
  extend ActiveSupport::Concern
6
6
  include Proscenium::CssModule
7
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
8
  end
27
9
  end
@@ -56,7 +56,7 @@ module Proscenium::Phlex::Page
56
56
  super do
57
57
  yield if block_given?
58
58
 
59
- side_load_javascripts defer: true, type: :module
59
+ side_load_javascripts type: :module, defer: true
60
60
  end
61
61
  end
62
62
  end
@@ -1,69 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Renders a div for use with @proscenium/component-manager.
4
+ # Renders a <div> for use with React components, with data attributes specifying the component path
5
+ # and props.
5
6
  #
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.
7
+ # If a block is given, it will be yielded within the div, allowing for a custom "loading" UI. If no
8
+ # block is given, then a "loading..." text will be rendered. It is intended that the component is
9
+ # mounted to this div, and the loading UI will then be replaced with the component's rendered
10
+ # output.
10
11
  #
11
- # React components are not side loaded at all.
12
+ # You can pass props to the component in the `:props` keyword argument.
12
13
  #
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
-
14
+ class Proscenium::Phlex::ReactComponent < Proscenium::Phlex
25
15
  self.abstract_class = true
26
16
 
17
+ include Proscenium::Componentable
27
18
  include Proscenium::Phlex::ComponentConcerns::CssModules
28
19
 
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.
20
+ # Override this to provide your own loading UI.
21
+ #
22
+ # @example
23
+ # def template(**attributes, &block)
24
+ # super do
25
+ # 'Look at me! I am loading now...'
26
+ # end
27
+ # end
28
+ #
29
+ # @yield the given block to a `div` within the top level component div.
41
30
  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)
31
+ send root_tag, **{ data: data_attributes }.deep_merge(attributes), &block
68
32
  end
69
33
  end
@@ -11,6 +11,9 @@ module Proscenium
11
11
 
12
12
  APPLICATION_INCLUDE_PATHS = ['config', 'app/assets', 'app/views', 'lib', 'node_modules'].freeze
13
13
 
14
+ # Environment variables that should always be passed to the builder.
15
+ DEFAULT_ENV_VARS = Set['RAILS_ENV', 'NODE_ENV'].freeze
16
+
14
17
  class << self
15
18
  def config
16
19
  @config ||= Railtie.config.proscenium
@@ -21,11 +24,18 @@ module Proscenium
21
24
  isolate_namespace Proscenium
22
25
 
23
26
  config.proscenium = ActiveSupport::OrderedOptions.new
27
+ config.proscenium.debug = false
24
28
  config.proscenium.side_load = true
29
+ config.proscenium.code_splitting = false
25
30
  config.proscenium.cache_query_string = Rails.env.production? && ENV.fetch('REVISION', nil)
26
31
  config.proscenium.cache_max_age = 2_592_000 # 30 days
27
32
  config.proscenium.include_paths = Set.new(APPLICATION_INCLUDE_PATHS)
28
33
 
34
+ # List of environment variable names that should be passed to the builder, which will then be
35
+ # passed to esbuild's `Define` option. Being explicit about which environment variables are
36
+ # defined means a faster build, as esbuild will have less to do.
37
+ config.proscenium.env_vars = Set.new
38
+
29
39
  # A hash of gems that can be side loaded. Assets from gems listed here can be side loaded.
30
40
  #
31
41
  # Because side loading uses URL paths, any gem dependencies that side load assets will fail,
@@ -5,16 +5,16 @@ class Proscenium::SideLoad
5
5
  def self.included(child)
6
6
  child.class_eval do
7
7
  append_after_action do
8
- if Proscenium::Current.loaded
8
+ if request.format.html? && Proscenium::Current.loaded
9
9
  if Proscenium::Current.loaded[:js].present?
10
- raise NotIncludedError, 'There are javascripts to be side loaded, but they have not ' \
11
- 'been included. Did you forget to add the ' \
10
+ raise NotIncludedError, 'There are javascripts to be side loaded, but they have ' \
11
+ 'not been included. Did you forget to add the ' \
12
12
  '`#side_load_javascripts` helper in your views?'
13
13
  end
14
14
 
15
15
  if Proscenium::Current.loaded[:css].present?
16
- raise NotIncludedError, 'There are stylesheets to be side loaded, but they have not ' \
17
- 'been included. Did you forget to add the ' \
16
+ raise NotIncludedError, 'There are stylesheets to be side loaded, but they have ' \
17
+ 'notbeen included. Did you forget to add the ' \
18
18
  '`#side_load_stylesheets` helper in your views?'
19
19
  end
20
20
  end
@@ -2,23 +2,39 @@
2
2
 
3
3
  module Proscenium
4
4
  module SideLoad::Helper
5
- def side_load_stylesheets
5
+ def side_load_stylesheets(**options)
6
6
  return unless Proscenium::Current.loaded
7
7
 
8
8
  out = []
9
9
  Proscenium::Current.loaded[:css].delete_if do |path|
10
- out << stylesheet_link_tag(path, extname: false)
10
+ out << stylesheet_link_tag(path, extname: false, **options)
11
11
  end
12
12
  out.join("\n").html_safe
13
13
  end
14
14
 
15
- def side_load_javascripts(**options)
15
+ def side_load_javascripts(**options) # rubocop:disable Metrics/AbcSize
16
16
  return unless Proscenium::Current.loaded
17
17
 
18
18
  out = []
19
- Proscenium::Current.loaded[:js].delete_if do |path|
20
- out << javascript_include_tag(path, extname: false, **options)
19
+ paths = Proscenium::Current.loaded[:js]
20
+
21
+ if Rails.application.config.proscenium.code_splitting && paths.size > 1
22
+ public_path = Rails.public_path.to_s
23
+ paths_to_build = []
24
+ paths.delete_if { |x| paths_to_build << x.delete_prefix('/') }
25
+
26
+ result = Proscenium::Builder.build(paths_to_build.join(';'), base_url: request.base_url)
27
+ result.split(';').each do |x|
28
+ next if x.include?('public/assets/_asset_chunks/') || x.end_with?('.map')
29
+
30
+ out << javascript_include_tag(x.delete_prefix(public_path), extname: false, **options)
31
+ end
32
+ else
33
+ paths.delete_if do |x|
34
+ out << javascript_include_tag(x, extname: false, **options)
35
+ end
21
36
  end
37
+
22
38
  out.join("\n").html_safe
23
39
  end
24
40
  end
@@ -18,7 +18,6 @@ class Proscenium::SideLoad
18
18
  Proscenium::SideLoad.append "app/views/#{renderable.virtual_path}"
19
19
  elsif template.respond_to?(:virtual_path) &&
20
20
  template.respond_to?(:type) && template.type == :html
21
- # Side load regular view template.
22
21
  Proscenium::SideLoad.append "app/views/#{layout.virtual_path}" if layout
23
22
 
24
23
  # Try side loading the variant template
@@ -26,7 +25,6 @@ class Proscenium::SideLoad
26
25
  Proscenium::SideLoad.append "app/views/#{template.virtual_path}+#{template.variant}"
27
26
  end
28
27
 
29
- # The variant template may not exist (above), so we try the regular non-variant path.
30
28
  Proscenium::SideLoad.append "app/views/#{template.virtual_path}"
31
29
  end
32
30
 
@@ -37,6 +35,16 @@ class Proscenium::SideLoad
37
35
  module PartialRenderer
38
36
  private
39
37
 
38
+ def render_partial_template(view, locals, template, layout, block)
39
+ if template.respond_to?(:virtual_path) &&
40
+ template.respond_to?(:type) && template.type == :html
41
+ Proscenium::SideLoad.append "app/views/#{layout.virtual_path}" if layout
42
+ Proscenium::SideLoad.append "app/views/#{template.virtual_path}"
43
+ end
44
+
45
+ super
46
+ end
47
+
40
48
  def build_rendered_template(content, template)
41
49
  path = Rails.root.join('app', 'views', template.virtual_path)
42
50
  cssm = Proscenium::CssModule::Resolver.new(path)
@@ -12,10 +12,11 @@ module Proscenium
12
12
 
13
13
  EXTENSIONS = %i[js css].freeze
14
14
  EXTENSION_MAP = {
15
+ '.module.css' => :css,
15
16
  '.css' => :css,
16
- # '.tsx' => :js,
17
+ '.tsx' => :js,
17
18
  '.ts' => :js,
18
- # '.jsx' => :js,
19
+ '.jsx' => :js,
19
20
  '.js' => :js
20
21
  }.freeze
21
22
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Proscenium
4
- VERSION = '0.9.1'
4
+ VERSION = '0.10.0'
5
5
  end
@@ -1,34 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #
4
- # Renders HTML markup suitable for use with @proscenium/component-manager.
4
+ # Renders a <div> for use with React components, with data attributes specifying the component path
5
+ # and props.
5
6
  #
6
7
  # If a content block is given, that content will be rendered inside the component, allowing for a
7
- # "loading" UI. If no block is given, then a loading text will be rendered.
8
- #
9
- # The parent div is not decorated with any attributes, apart from the selector class required by
10
- # component-manager. But if your component has a side loaded CSS module stylesheet
11
- # (component.module.css), with a `.component` class defined, then that class will be assigned to the
12
- # parent div as a CSS module.
8
+ # "loading" UI. If no block is given, then a "loading..." text will be rendered. It is intended that
9
+ # the component is mounted to this div, and the loading UI will then be replaced with the
10
+ # component's rendered output.
13
11
  #
14
12
  class Proscenium::ViewComponent::ReactComponent < Proscenium::ViewComponent
15
13
  self.abstract_class = true
16
14
 
17
- attr_accessor :props, :lazy
18
-
19
- # @param props: [Hash]
20
- # @param lazy: [Boolean] Lazy load the component using IntersectionObserver. Default: true.
21
- # @param [Block]
22
- def initialize(props: {}, lazy: true)
23
- @props = props
24
- @lazy = lazy
25
-
26
- super
27
- end
15
+ include Proscenium::Componentable
28
16
 
29
17
  def call
30
- tag.div class: ['componentManagedByProscenium', css_module(:component)],
31
- data: { component: { path: virtual_path, props: props, lazy: lazy } } do
18
+ tag.send root_tag, data: data_attributes do
32
19
  tag.div content || 'loading...'
33
20
  end
34
21
  end
@@ -10,10 +10,17 @@ class Proscenium::ViewComponent < ViewComponent::Base
10
10
  autoload :ReactComponent
11
11
 
12
12
  # Side loads the class, and its super classes that respond to `.path`. Assign the `abstract_class`
13
- # class variable to any abstract class, and it will not be side loaded.
13
+ # class variable to any abstract class, and it will not be side loaded. Additionally, if the class
14
+ # responds to `side_load`, then that method is called.
14
15
  module Sideload
15
16
  def before_render
16
17
  klass = self.class
18
+
19
+ if !klass.abstract_class && respond_to?(:side_load, true)
20
+ side_load
21
+ klass = klass.superclass
22
+ end
23
+
17
24
  while !klass.abstract_class && klass.respond_to?(:path) && klass.path
18
25
  Proscenium::SideLoad.append klass.path
19
26
  klass = klass.superclass
data/lib/proscenium.rb CHANGED
@@ -9,10 +9,11 @@ module Proscenium
9
9
  autoload :Middleware
10
10
  autoload :SideLoad
11
11
  autoload :CssModule
12
+ autoload :Componentable
12
13
  autoload :ViewComponent
13
14
  autoload :Phlex
14
15
  autoload :Helper
15
- autoload :Esbuild
16
+ autoload :Builder
16
17
 
17
18
  def self.reset_current_side_loaded
18
19
  Current.reset
@@ -59,7 +60,7 @@ module Proscenium
59
60
  relpath = path.delete_prefix(sroot)
60
61
 
61
62
  if (package_name = matched_gem[1][:package_name] || matched_gem[0])
62
- return Esbuild::Golib.resolve("#{package_name}/#{relpath}")
63
+ return Builder.resolve("#{package_name}/#{relpath}")
63
64
  end
64
65
 
65
66
  # TODO: manually resolve the path without esbuild
@@ -68,7 +69,7 @@ module Proscenium
68
69
 
69
70
  return path.delete_prefix(Rails.root.to_s) if path.starts_with?("#{Rails.root}/")
70
71
 
71
- Esbuild::Golib.resolve(path)
72
+ Builder.resolve(path)
72
73
  end
73
74
 
74
75
  # Resolves CSS class `names` to CSS module names. Each name will be converted to a CSS module
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.9.1
4
+ version: 0.10.0
5
5
  platform: x86_64-darwin
6
6
  authors:
7
7
  - Joel Moss
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-11 00:00:00.000000000 Z
11
+ date: 2023-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -72,34 +72,6 @@ dependencies:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
74
  version: '3.13'
75
- - !ruby/object:Gem::Dependency
76
- name: phlex
77
- requirement: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: 1.8.1
82
- type: :runtime
83
- prerelease: false
84
- version_requirements: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - "~>"
87
- - !ruby/object:Gem::Version
88
- version: 1.8.1
89
- - !ruby/object:Gem::Dependency
90
- name: phlex-rails
91
- requirement: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - "~>"
94
- - !ruby/object:Gem::Version
95
- version: 1.0.0
96
- type: :runtime
97
- prerelease: false
98
- version_requirements: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - "~>"
101
- - !ruby/object:Gem::Version
102
- version: 1.0.0
103
75
  - !ruby/object:Gem::Dependency
104
76
  name: railties
105
77
  requirement: !ruby/object:Gem::Requirement
@@ -131,15 +103,16 @@ files:
131
103
  - LICENSE.txt
132
104
  - README.md
133
105
  - lib/proscenium.rb
106
+ - lib/proscenium/builder.rb
107
+ - lib/proscenium/componentable.rb
134
108
  - lib/proscenium/css_module.rb
135
109
  - lib/proscenium/css_module/class_names_resolver.rb
136
110
  - lib/proscenium/css_module/resolver.rb
137
111
  - lib/proscenium/current.rb
138
- - lib/proscenium/esbuild.rb
139
- - lib/proscenium/esbuild/golib.rb
140
112
  - lib/proscenium/ext/proscenium
141
113
  - lib/proscenium/ext/proscenium.h
142
114
  - lib/proscenium/helper.rb
115
+ - lib/proscenium/libs/stimulus-loading.js
143
116
  - lib/proscenium/log_subscriber.rb
144
117
  - lib/proscenium/middleware.rb
145
118
  - lib/proscenium/middleware/base.rb
@@ -1,32 +0,0 @@
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