proscenium 0.9.1-x86_64-linux → 0.10.0-x86_64-linux
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +266 -42
- data/lib/proscenium/{esbuild/golib.rb → builder.rb} +42 -17
- data/lib/proscenium/componentable.rb +63 -0
- data/lib/proscenium/ext/proscenium +0 -0
- data/lib/proscenium/ext/proscenium.h +19 -12
- data/lib/proscenium/libs/stimulus-loading.js +83 -0
- data/lib/proscenium/log_subscriber.rb +1 -2
- data/lib/proscenium/middleware/base.rb +1 -1
- data/lib/proscenium/middleware/esbuild.rb +2 -2
- data/lib/proscenium/middleware.rb +5 -0
- data/lib/proscenium/phlex/component_concerns.rb +0 -18
- data/lib/proscenium/phlex/page.rb +1 -1
- data/lib/proscenium/phlex/react_component.rb +20 -56
- data/lib/proscenium/railtie.rb +10 -0
- data/lib/proscenium/side_load/ensure_loaded.rb +5 -5
- data/lib/proscenium/side_load/helper.rb +21 -5
- data/lib/proscenium/side_load/monkey.rb +10 -2
- data/lib/proscenium/side_load.rb +3 -2
- data/lib/proscenium/version.rb +1 -1
- data/lib/proscenium/view_component/react_component.rb +7 -20
- data/lib/proscenium/view_component.rb +8 -1
- data/lib/proscenium.rb +4 -3
- metadata +5 -32
- data/lib/proscenium/esbuild.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 56b426c6cecb6a9f951863c43cf2481da79a3dd9c8e68178e5af574819d43670
|
4
|
+
data.tar.gz: 1c51042422b33d92432ea0620e7eed761499dfc217b9fc6f3e87038b83201e76
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2eb6f29a59761899c12e98ce25fad39109aef3878a7afa053851a6387433108ff8a79948b2fd083c8530ef9da83ed8c60fc02851df442bbe517f6663beb6dc03
|
7
|
+
data.tar.gz: ef7fd29013ec3ce60044a315d17b0cdb2aa738aefe39b16273abbf9636773f0048a9c1cf9d6d8570b28f5609bed4de442e2697106bf90c61c993a9266317a618
|
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
|
-
-
|
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
|
-
-
|
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
|
-
|
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
|
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
|
-
|
92
|
+
## Side Loading
|
57
93
|
|
58
|
-
|
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
|
-
|
96
|
+
Proscenium is best experienced when you side load your assets.
|
61
97
|
|
62
|
-
|
63
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
177
|
+
<%= side_load_javascripts type: 'module', defer: true %>
|
85
178
|
</body>
|
86
179
|
</html>
|
87
180
|
```
|
88
181
|
|
89
|
-
|
90
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
232
|
+
## Import Maps
|
233
|
+
|
234
|
+
> **[WIP]**
|
138
235
|
|
139
|
-
[Import
|
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
|
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
|
-
##
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
##
|
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
|
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('
|
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
|
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
|
55
|
-
|
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
|
64
|
-
|
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, @
|
69
|
-
|
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
|
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
|
-
//
|
89
|
-
//
|
90
|
-
//
|
91
|
-
//
|
92
|
-
//
|
93
|
-
//
|
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*
|
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
|
-
//
|
100
|
-
//
|
101
|
-
//
|
102
|
-
//
|
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
|
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/)
|
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::
|
24
|
+
render_response Proscenium::Builder.build(path_to_build, root: root,
|
25
25
|
base_url: @request.base_url)
|
26
26
|
end
|
27
|
-
rescue Proscenium::
|
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
|
@@ -1,69 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Renders a div for use with
|
4
|
+
# Renders a <div> for use with React components, with data attributes specifying the component path
|
5
|
+
# and props.
|
5
6
|
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
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
|
-
#
|
12
|
+
# You can pass props to the component in the `:props` keyword argument.
|
12
13
|
#
|
13
|
-
class Proscenium::Phlex::ReactComponent < Phlex
|
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
|
-
|
30
|
-
|
31
|
-
# @
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
37
|
-
|
38
|
-
# @yield the given block to a `div` within the top level component div.
|
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
|
-
|
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
|
data/lib/proscenium/railtie.rb
CHANGED
@@ -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
|
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
|
17
|
-
'
|
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]
|
20
|
-
|
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)
|
data/lib/proscenium/side_load.rb
CHANGED
data/lib/proscenium/version.rb
CHANGED
@@ -1,34 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
#
|
4
|
-
# Renders
|
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
|
-
#
|
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
|
-
|
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.
|
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 :
|
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
|
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
|
-
|
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.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: x86_64-linux
|
6
6
|
authors:
|
7
7
|
- Joel Moss
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
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
|
data/lib/proscenium/esbuild.rb
DELETED
@@ -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
|