proscenium 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +84 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +908 -0
  5. data/lib/proscenium/builder.rb +189 -0
  6. data/lib/proscenium/core_ext/object/css_module_ivars.rb +19 -0
  7. data/lib/proscenium/css_module/path.rb +31 -0
  8. data/lib/proscenium/css_module/rewriter.rb +44 -0
  9. data/lib/proscenium/css_module/transformer.rb +84 -0
  10. data/lib/proscenium/css_module.rb +57 -0
  11. data/lib/proscenium/ensure_loaded.rb +27 -0
  12. data/lib/proscenium/ext/proscenium +0 -0
  13. data/lib/proscenium/ext/proscenium.h +131 -0
  14. data/lib/proscenium/helper.rb +70 -0
  15. data/lib/proscenium/importer.rb +134 -0
  16. data/lib/proscenium/libs/custom_element.js +54 -0
  17. data/lib/proscenium/libs/react-manager/index.jsx +121 -0
  18. data/lib/proscenium/libs/react-manager/react.js +2 -0
  19. data/lib/proscenium/libs/stimulus-loading.js +65 -0
  20. data/lib/proscenium/libs/test.js +1 -0
  21. data/lib/proscenium/libs/ujs/class.js +15 -0
  22. data/lib/proscenium/libs/ujs/data_confirm.js +23 -0
  23. data/lib/proscenium/libs/ujs/data_disable_with.js +68 -0
  24. data/lib/proscenium/libs/ujs/index.js +9 -0
  25. data/lib/proscenium/log_subscriber.rb +37 -0
  26. data/lib/proscenium/middleware/base.rb +103 -0
  27. data/lib/proscenium/middleware/engines.rb +45 -0
  28. data/lib/proscenium/middleware/esbuild.rb +30 -0
  29. data/lib/proscenium/middleware/runtime.rb +18 -0
  30. data/lib/proscenium/middleware/url.rb +16 -0
  31. data/lib/proscenium/middleware.rb +76 -0
  32. data/lib/proscenium/monkey.rb +95 -0
  33. data/lib/proscenium/phlex/asset_inclusions.rb +17 -0
  34. data/lib/proscenium/phlex/css_modules.rb +79 -0
  35. data/lib/proscenium/phlex/react_component.rb +32 -0
  36. data/lib/proscenium/phlex.rb +42 -0
  37. data/lib/proscenium/railtie.rb +106 -0
  38. data/lib/proscenium/react_componentable.rb +95 -0
  39. data/lib/proscenium/resolver.rb +39 -0
  40. data/lib/proscenium/side_load.rb +155 -0
  41. data/lib/proscenium/source_path.rb +15 -0
  42. data/lib/proscenium/templates/rescues/build_error.html.erb +30 -0
  43. data/lib/proscenium/ui/breadcrumbs/component.module.css +14 -0
  44. data/lib/proscenium/ui/breadcrumbs/component.rb +79 -0
  45. data/lib/proscenium/ui/breadcrumbs/computed_element.rb +69 -0
  46. data/lib/proscenium/ui/breadcrumbs/control.rb +95 -0
  47. data/lib/proscenium/ui/breadcrumbs/mixins.css +83 -0
  48. data/lib/proscenium/ui/breadcrumbs.rb +72 -0
  49. data/lib/proscenium/ui/component.rb +11 -0
  50. data/lib/proscenium/ui/test.js +1 -0
  51. data/lib/proscenium/ui.rb +14 -0
  52. data/lib/proscenium/utils.rb +13 -0
  53. data/lib/proscenium/version.rb +5 -0
  54. data/lib/proscenium/view_component/css_modules.rb +11 -0
  55. data/lib/proscenium/view_component/react_component.rb +22 -0
  56. data/lib/proscenium/view_component/sideload.rb +4 -0
  57. data/lib/proscenium/view_component.rb +38 -0
  58. data/lib/proscenium.rb +70 -0
  59. metadata +228 -0
data/README.md ADDED
@@ -0,0 +1,908 @@
1
+ # Proscenium - Modern client-side development for Rails
2
+
3
+ Proscenium treats your client-side code as first class citizens of your Rails app, and assumes a "fast by default" internet. It bundles your JavaScript and CSS in real time, on demand, and with zero configuration.
4
+
5
+ **The highlights:**
6
+
7
+ - Fast real-time bundling, tree-shaking, code-splitting and minification of Javascript (.js,.jsx), Typescript (.ts,.tsx) and CSS (.css).
8
+ - NO JavaScript runtime needed - just the browser!
9
+ - NO build step or pre-compilation.
10
+ - NO additional process or server - Just run Rails!
11
+ - Deep integration with Rails.
12
+ - Automatically side-load your layouts, views, and partials.
13
+ - Import from NPM, URL's, and locally.
14
+ - Server-side import map support.
15
+ - CSS Modules & mixins.
16
+ - Source maps.
17
+
18
+ ## Table of Contents
19
+
20
+ - [Getting Started](#getting-started)
21
+ - [Installation](#installation)
22
+ - [Client-Side Code Anywhere](#client-side-code-anywhere)
23
+ - [Side Loading](#side-loading)
24
+ - [Importing](#importing-assets)
25
+ - [URL Imports](#url-imports)
26
+ - [Local Imports](#local-imports)
27
+ - [Import Maps](#import-maps)
28
+ - [Source Maps](#source-maps)
29
+ - [SVG](#svg)
30
+ - [Environment Variables](#environment-variables)
31
+ - [i18n](#i18n)
32
+ - [JavaScript](#javascript)
33
+ - [Tree Shaking](#tree-shaking)
34
+ - [Code Splitting](#code-splitting)
35
+ - [JavaScript Caveats](#javascript-caveats)
36
+ - [CSS](#css)
37
+ - [Importing CSS from JavaScript](#importing-css-from-javascript)
38
+ - [CSS Modules](#css-modules)
39
+ - [CSS Mixins](#css-mixins)
40
+ - [CSS Caveats](#css-caveats)
41
+ - [Typescript](#typescript)
42
+ - [Typescript Caveats](#typescript-caveats)
43
+ - [JSX](#jsx)
44
+ - [JSON](#json)
45
+ - [Phlex Support](#phlex-support)
46
+ - [ViewComponent Support](#viewcomponent-support)
47
+ - [Cache Busting](#cache-busting)
48
+ - [rjs is back!](#rjs-is-back)
49
+ - [Resolution](#resolution)
50
+ - [Assets from Rails Engines](#assets-from-rails-engines)
51
+ - [Thanks](#thanks)
52
+ - [Development](#development)
53
+
54
+ ## Getting Started
55
+
56
+ Getting started obviously depends on whether you are adding Proscenium to an existing Rails app, or creating a new one. So choose the appropriate guide below:
57
+
58
+ - [Getting Started with a new Rails app](https://github.com/joelmoss/proscenium/blob/master/docs/guides/new_rails_app.md)
59
+ - Getting Started with an existing Rails app
60
+ - [Migrate from Sprockets](docs/guides/migrate_from_sprockets.md)
61
+ - Migrate from Propshaft _[Coming soon]_
62
+ - Migrate from Webpacker _[Coming soon]_
63
+ - [Render a React component with Proscenium](docs/guides/basic_react.md)
64
+
65
+ ## Installation
66
+
67
+ Add this line to your Rails application's Gemfile, and you're good to go:
68
+
69
+ ```ruby
70
+ gem 'proscenium'
71
+ ```
72
+
73
+ Please note that Proscenium is designed solely for use with Rails, so will not work - at least out of the box - anywhere else.
74
+
75
+ ## Client-Side Code Anywhere
76
+
77
+ Proscenium believes that your frontend code is just as important as your backend code, and is not an afterthought - they should be first class citizens of your Rails app. So instead of having to throw all your JS and CSS into a "app/assets" directory, and then requiring a separate process to compile or bundle, just put them wherever you want within your app, and just run Rails!
78
+
79
+ For example, if you have some JS that is required by your `app/views/users/index.html.erb` view, just create a JS file alongside it at `app/views/users/index.js`. Or if you have some CSS that is used by your entire application, put it in `app/views/layouts/application.css` and load it alongside your layout. Maybe you have a few JS utility functions, so put them in `lib/utils.js`.
80
+
81
+ Simply put your JS(X) and CSS anywhere you want, and they will be served by your Rails app from the location where you placed them.
82
+
83
+ Using the examples above...
84
+
85
+ - `app/views/users/index.js` => `https://yourapp.com/app/views/users/index.js`
86
+ - `app/views/layouts/application.css` => `https://yourapp.com/app/views/layouts/application.css`
87
+ - `lib/utils.js` => `https://yourapp.com/lib/utils.js`
88
+ - `app/components/menu_component.jsx` => `https://yourapp.com/app/components/menu_component.jsx`
89
+ - `config/properties.css` => `https://yourapp.com/config/properties.css`
90
+
91
+ ## Side Loading
92
+
93
+ > 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`, `.tsx`, and `.module.css`. Also partials were not side loaded prior to 0.10.0.
94
+
95
+ Proscenium is best experienced when you side load your assets.
96
+
97
+ ### The Problem
98
+
99
+ With Rails you would typically declaratively load your JavaScript and CSS assets using the `javascript_include_tag` and `stylesheet_link_tag` helpers.
100
+
101
+ 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`.
102
+
103
+ You would manually and declaratively include those two files in your application layout, something like this:
104
+
105
+ ```erb
106
+ <%# /app/views/layouts/application.html.erb %>
107
+
108
+ <!DOCTYPE html>
109
+ <html>
110
+ <head>
111
+ <title>Hello World</title>
112
+ <%= stylesheet_link_tag 'application' %> <!-- << Your app CSS -->
113
+ </head>
114
+ <body>
115
+ <%= yield %>
116
+ <%= javascript_include_tag 'application' %> <!-- << Your app JS -->
117
+ </body>
118
+ </html>
119
+ ```
120
+
121
+ 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 (or layout), something like this:
122
+
123
+ ```erb
124
+ <%# /app/views/users/index.html.erb %>
125
+
126
+ <%= stylesheet_link_tag 'users' %>
127
+ <%= javascript_include_tag 'users' %>
128
+
129
+ <%# needed by the `users/_user.html.erb` partial %>
130
+ <%= javascript_include_tag '_user' %>
131
+
132
+ <% render @users %>
133
+ ```
134
+
135
+ 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.
136
+
137
+ ### The Solution
138
+
139
+ 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.
140
+
141
+ 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.
142
+
143
+ JSX is also supported for JavaScript and Typescript. Simply use the `.jsx` or `.tsx` extension instead of `.js` or `.ts`.
144
+
145
+ ### Usage
146
+
147
+ Simply create a JS and/or CSS file with the same name as any view, partial or layout.
148
+
149
+ Let's continue with our problem example above, where we have the following assets
150
+
151
+ - `/app/assets/application.css`
152
+ - `/app/assets/application.js`
153
+ - `/app/assets/users.css`
154
+ - `/app/assets/users.js`
155
+ - `/app/assets/user.js`
156
+
157
+ 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:
158
+
159
+ - `/app/views/layouts/application.css`
160
+ - `/app/views/layouts/application.js`
161
+ - `/app/views/users/index.css`
162
+ - `/app/views/users/index.js`
163
+ - `/app/views/users/_user.js` (partial)
164
+
165
+ Now, in your layout and view, replace the `javascript_include_tag` and `stylesheet_link_tag` helpers with the `include_asset` helper from Proscenium. Something like this:
166
+
167
+ ```erb
168
+ <!DOCTYPE html>
169
+ <html>
170
+ <head>
171
+ <title>Hello World</title>
172
+ <%= include_assets # <-- %>
173
+ </head>
174
+ <body>
175
+ <%= yield %>
176
+ </body>
177
+ </html>
178
+ ```
179
+
180
+ On each page request, Proscenium will check if any of your views, layouts and partials have a
181
+ JS/TS/CSS file of the same name, and then include them wherever your placed the `include_assets`
182
+ helper.
183
+
184
+ Now you never have to remember to include your assets again. Just create them alongside your views,
185
+ partials and layouts, and Proscenium will take care of the rest.
186
+
187
+ Side loading is enabled by default, but you can disable it by setting `config.proscenium.side_load`
188
+ to `false` in your `/config/application.rb`.
189
+
190
+ There are also `include_stylesheets` and `include_javascripts` helpers to allow you to control where
191
+ the CSS and JS assets are included in the HTML. These helpers should be used instead of
192
+ `include_assets` if you want to control exactly where the assets are included.
193
+
194
+ ## Importing Assets
195
+
196
+ Proscenium supports importing JS, JSX, TS, TSX, CSS and SVG from NPM, by URL, your local app, and even from Ruby Gems.
197
+
198
+ Imported files are bundled together in real time. So no build step or pre-compilation is needed.
199
+
200
+ Imports are assumed to be JS files, so there is no need to specify the file extesnion in such cases. But you can if you like. All other file types must be specified using their full file name and extension.
201
+
202
+ ### URL Imports
203
+
204
+ Any import beginning with `http://` or `https://` will be fetched from the URL provided. For example:
205
+
206
+ ```js
207
+ import React from "https://esm.sh/react";
208
+ ```
209
+
210
+ ```css
211
+ @import "https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css";
212
+ ```
213
+
214
+ URL imports are cached, so that each import is only fetched once per server restart.
215
+
216
+ ### Import from NPM (`node_modules`)
217
+
218
+ Bare imports (imports not beginning with `./`, `/`, `https://`, `http://`) are fully supported, and will use your package manager of choice (eg, NPM, Yarn, pnpm) via the `package.json` file:
219
+
220
+ ```js
221
+ import React from "react";
222
+ ```
223
+
224
+ ### Local Imports
225
+
226
+ And of course you can import your own code, using relative or absolute paths (file extension is optional):
227
+
228
+ ```js /app/views/layouts/application.js
229
+ import utils from "/lib/utils";
230
+ ```
231
+
232
+ ```js /lib/utils.js
233
+ import constants from "./constants";
234
+ ```
235
+
236
+ ```css /app/views/layouts/application.css
237
+ @import "/lib/reset";
238
+ ```
239
+
240
+ ```css /lib/reset.css
241
+ body {
242
+ /* some styles... */
243
+ }
244
+ ```
245
+
246
+ ### Unbundling
247
+
248
+ Sometimes you don't want to bundle an import. For example, you want to ensure that only one instance of React is loaded. In this cases, you can use the `unbundle` prefix
249
+
250
+ ```js
251
+ import React from "unbundle:react";
252
+ ```
253
+
254
+ This only works any bare and local imports.
255
+
256
+ You can also use the `unbundle` prefix in your import map, which ensures that all imports of a particular path is always unbundled:
257
+
258
+ ```json
259
+ {
260
+ "imports": {
261
+ "react": "unbundle:react"
262
+ }
263
+ }
264
+ ```
265
+
266
+ Then just import as normal:
267
+
268
+ ```js
269
+ import React from "react";
270
+ ```
271
+
272
+ ## Import Maps
273
+
274
+ > **[WIP]**
275
+
276
+ [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.
277
+
278
+ If you are not familiar with import maps, think of them as a way to define aliases.
279
+
280
+ Just create `config/import_map.json` and specify the imports you want to use. For example:
281
+
282
+ ```json
283
+ {
284
+ "imports": {
285
+ "react": "https://esm.sh/react@18.2.0",
286
+ "start": "/lib/start.js",
287
+ "common": "/lib/common.css",
288
+ "@radix-ui/colors/": "https://esm.sh/@radix-ui/colors@0.1.8/"
289
+ }
290
+ }
291
+ ```
292
+
293
+ Using the above import map, we can do...
294
+
295
+ ```js
296
+ import { useCallback } from "react";
297
+ import startHere from "start";
298
+ import styles from "common";
299
+ ```
300
+
301
+ and for CSS...
302
+
303
+ ```css
304
+ @import "common";
305
+ @import "@radix-ui/colors/blue.css";
306
+ ```
307
+
308
+ 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.
309
+
310
+ ```js
311
+ (env) => ({
312
+ imports: {
313
+ react:
314
+ env === "development"
315
+ ? "https://esm.sh/react@18.2.0?dev"
316
+ : "https://esm.sh/react@18.2.0",
317
+ },
318
+ });
319
+ ```
320
+
321
+ ## Source Maps
322
+
323
+ 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.
324
+
325
+ Source map output is supported for both JavaScript and CSS. Each file is appended with the link to the source map. For example:
326
+
327
+ ```js
328
+ //# sourceMappingURL=/app/views/layouts/application.js.map
329
+ ```
330
+
331
+ Your browsers dev tools should pick this up and automatically load the source map when and where needed.
332
+
333
+ ## SVG
334
+
335
+ 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.
336
+
337
+ ## Environment Variables
338
+
339
+ > Available in `>=0.10.0`
340
+
341
+ You can define and access any environment variable from your JavaScript and Typescript under the `proscenium.env` namespace.
342
+
343
+ For performance and security reasons you must declare the environment variable names that you wish to expose in your `config/application.rb` file.
344
+
345
+ ```ruby
346
+ config.proscenium.env_vars = Set['API_KEY', 'SOME_SECRET_VARIABLE']
347
+ config.proscenium.env_vars << 'ANOTHER_API_KEY'
348
+ ```
349
+
350
+ 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.
351
+
352
+ These declared environment variables will be replaced with constant expressions, allowing you to use this like this:
353
+
354
+ ```js
355
+ console.log(proscenium.env.RAILS_ENV); // console.log("development")
356
+ console.log(proscenium.env.RAILS_ENV === "development"); // console.log(true)
357
+ ```
358
+
359
+ The `RAILS_ENV` and `NODE_ENV` environment variables will always automatically be declared for you.
360
+
361
+ 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.
362
+
363
+ Environment variables are particularly powerful in aiding [tree shaking](#tree-shaking).
364
+
365
+ ```js
366
+ function start() {
367
+ console.log("start");
368
+ }
369
+ function doSomethingDangerous() {
370
+ console.log("resetDatabase");
371
+ }
372
+
373
+ proscenium.env.RAILS_ENV === "development" && doSomethingDangerous();
374
+
375
+ start();
376
+ ```
377
+
378
+ In development the above code will be transformed into the following code, discarding the definition, and call to`doSomethingDangerous()`.
379
+
380
+ ```js
381
+ function start() {
382
+ console.log("start");
383
+ }
384
+ start();
385
+ ```
386
+
387
+ Please note that for security reasons environment variables are not replaced in URL imports.
388
+
389
+ An undefined environment variable will be replaced with `undefined`.
390
+
391
+ ```js
392
+ console.log(proscenium.env.UNKNOWN); // console.log((void 0).UNKNOWN)
393
+ ```
394
+
395
+ 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):
396
+
397
+ ```js
398
+ if (typeof proscenium.env?.UNKNOWN !== "undefined") {
399
+ // do something if UNKNOWN is defined
400
+ }
401
+ ```
402
+
403
+ ## i18n
404
+
405
+ Basic support is provided for importing your Rails locale files from `config/locales/*.yml`, exporting them as JSON.
406
+
407
+ ```js
408
+ import translations from "@proscenium/i18n";
409
+ // translations.en.*
410
+ ```
411
+
412
+ ## Javascript
413
+
414
+ By default, Proscenium's output will take advantage of all modern JS features. For example, `a !== void 0 && a !== null ? a : b` will become `a ?? b` when minifying (enabled by default in production), which makes use of syntax from the ES2020 version of JavaScript.
415
+
416
+ ### Tree Shaking
417
+
418
+ Tree shaking is the term the JavaScript community uses for dead code elimination, a common compiler optimization that automatically removes unreachable code. Tree shaking is enabled by default in Proscenium.
419
+
420
+ ```javascript
421
+ function one() {
422
+ console.log("one");
423
+ }
424
+ function two() {
425
+ console.log("two");
426
+ }
427
+ one();
428
+ ```
429
+
430
+ The above code will be transformed to the following code, discarding `two()`, as it is never called.
431
+
432
+ ```javascript
433
+ function one() {
434
+ console.log("one");
435
+ }
436
+ one();
437
+ ```
438
+
439
+ ### Code Splitting
440
+
441
+ > Available in `>=0.10.0`.
442
+
443
+ [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.
444
+
445
+ As an example:
446
+
447
+ ```js
448
+ // /lib/son.js
449
+ import father from "./father";
450
+
451
+ father() + " and Son";
452
+ ```
453
+
454
+ ```js
455
+ // /lib/daughter.js
456
+ import father from "./father";
457
+
458
+ father() + " and Daughter";
459
+ ```
460
+
461
+ ```js
462
+ // /lib/father.js
463
+ export default () => "Father";
464
+ ```
465
+
466
+ 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.
467
+
468
+ If these files are side loaded, then `father.js` will be split off into a separate file or chunk, and only downloaded once.
469
+
470
+ - 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.
471
+
472
+ - 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.
473
+
474
+ - 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.
475
+
476
+ Code splitting is enabled by default. You can disable it by setting the `code_splitting` configuration option to `false` in your application's `/config/application.rb`:
477
+
478
+ ```ruby
479
+ config.proscenium.code_splitting = false
480
+ ```
481
+
482
+ ### JavaScript Caveats
483
+
484
+ 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).
485
+
486
+ ## CSS
487
+
488
+ CSS is a first-class content type in Proscenium, which means it can bundle CSS files directly without needing to import your CSS from JavaScript code. You can `@import` other CSS files and reference image and font files with `url()` and Proscenium will bundle everything together.
489
+
490
+ Note that by default, Proscenium's output will take advantage of all modern CSS features. For example, `color: rgba(255, 0, 0, 0.4)` will become `color: #f006` after minifying in production, which makes use of syntax from [CSS Color Module Level 4](https://drafts.csswg.org/css-color-4/#changes-from-3).
491
+
492
+ The new CSS nesting syntax is supported, and transformed into non-nested CSS for older browsers.
493
+
494
+ ### Importing CSS from JavaScript
495
+
496
+ 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.
497
+
498
+ ```jsx
499
+ import "./button.css";
500
+
501
+ export let Button = ({ text }) => {
502
+ return <div className="button">{text}</div>;
503
+ };
504
+ ```
505
+
506
+ ### CSS Modules
507
+
508
+ Proscenium implements a subset of [CSS Modules](https://github.com/css-modules/css-modules). It supports the `:local` and `:global` keywords, but not the `composes` property. (it is recommended that you use mixins instead of `composes`, as they will work everywhere, even in plain CSS files.)
509
+
510
+ Give any CSS file a `.module.css` extension, and Proscenium will treat it as a CSS Module, transforming all class names with a suffix unique to the file.
511
+
512
+ ```css
513
+ .title {
514
+ font-size: 20em;
515
+ }
516
+ ```
517
+
518
+ The above input produces:
519
+
520
+ ```css
521
+ .title-5564cdbb {
522
+ font-size: 20em;
523
+ }
524
+ ```
525
+
526
+ You now have a unique class name that you can use pretty much anywhere.
527
+
528
+ #### In your Views
529
+
530
+ You can reference CSS modules from your Rails views, partials, and layouts using the `css_module` helper, which accepts one or more class names, and will return the equivilent CSS module names - the class name with the unique suffix appended.
531
+
532
+ With [side-loading](#side-loading) setup, you can use the `css_module` helper as follows.
533
+
534
+ ```erb
535
+ <div>
536
+ <h1 class="<%= css_module :hello_title %>">Hello World</h1>
537
+ <p class="<%= css_module :body, paragraph: %>">
538
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
539
+ </p>
540
+ </div>
541
+ ```
542
+
543
+ `css_module` accepts multiple class names, and will return a space-separated string of transformed CSS module names.
544
+
545
+ ```ruby
546
+ css_module :my_module_name
547
+ # => "my_module_name-ABCD1234"
548
+ ```
549
+
550
+ You can even reference a class from any CSS file by passing the URL path to the file, as a prefix to the class name. Doing so will automatically [side load](#side-loading) the stylesheet.
551
+
552
+ ```ruby
553
+ css_module '/app/components/button.css@big_button'
554
+ # => "big_button"
555
+ ```
556
+
557
+ It also supports NPM packages (already installed in /node_modules):
558
+
559
+ ```ruby
560
+ css_module 'mypackage/button@big_button'
561
+ # => "big_button"
562
+ ```
563
+
564
+ `css_module` also accepts a `path` keyword argument, which allows you to specify the path to the CSS
565
+ file. Note that this will use the given path for all class names passed to that instance of `css_module`.
566
+
567
+ ```ruby
568
+ css_module :my_module_name, path: Rails.root.join('app/components/button.css')
569
+ ```
570
+
571
+ #### In your JavaScript
572
+
573
+ Importing a CSS module from JS will automatically append the stylesheet to the document's head. And the result of the import will be an object of CSS class to module names.
574
+
575
+ ```js
576
+ import styles from "./styles.module.css";
577
+ // styles == { header: 'header-5564cdbb' }
578
+ ```
579
+
580
+ It is important to note that the exported object of CSS module names is actually a JavaScript [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) object. So destructuring the object will not work. Instead, you must access the properties directly.
581
+
582
+ Also, importing a CSS module into another CSS module will result in the same digest string for all classes.
583
+
584
+ ### CSS Mixins
585
+
586
+ Proscenium provides functionality for including or "mixing in" onr or more CSS classes into another. This is similar to the `composes` property of CSS Modules, but works everywhere, and is not limited to CSS Modules.
587
+
588
+ CSS mixins are supported using the `@define-mixin` and `@mixin` at-rules.
589
+
590
+ A mixin is defined using the `@define-mixin` at-rule. Pass it a name, which should adhere to class name semantics, and declare your rules:
591
+
592
+ ```css
593
+ // /lib/mixins.css
594
+ @define-mixin bigText {
595
+ font-size: 50px;
596
+ }
597
+ ```
598
+
599
+ Use a mixin using the `@mixin` at-rule. Pass it the name of the mixin you want to use, and the url where the mixin is declared. The url is used to resolve the mixin, and can be relative, absolute, a URL, or even from an NPM packacge.
600
+
601
+ ```css
602
+ // /app/views/layouts/application.css
603
+ p {
604
+ @mixin bigText from url("/lib/mixins.css");
605
+ color: red;
606
+ }
607
+ ```
608
+
609
+ The above produce this output:
610
+
611
+ ```css
612
+ p {
613
+ font-size: 50px;
614
+ color: red;
615
+ }
616
+ ```
617
+
618
+ Mixins can be declared in any CSS file. They do not need to be declared in the same file as where they are used. however, if you declare and use a mixin in the same file, you don't need to specify the URL of where the mixin is declared.
619
+
620
+ ```css
621
+ @define-mixin bigText {
622
+ font-size: 50px;
623
+ }
624
+
625
+ p {
626
+ @mixin bigText;
627
+ color: red;
628
+ }
629
+ ```
630
+
631
+ CSS modules and Mixins works perfectly together. You can include a mixin in a CSS module.
632
+
633
+ ### CSS Caveats
634
+
635
+ There are a few important caveats as far as CSS is concerned. These are [detailed on the esbuild site](https://esbuild.github.io/content-types/#css-caveats).
636
+
637
+ ## Typescript
638
+
639
+ Typescript and TSX is supported out of the box, and has built-in support for parsing TypeScript syntax and discarding the type annotations. Just rename your files to `.ts` or `.tsx` and you're good to go.
640
+
641
+ Please note that Proscenium does not do any type checking so you will still need to run `tsc -noEmit` in parallel with Proscenium to check types.
642
+
643
+ ### Typescript Caveats
644
+
645
+ There are a few important caveats as far as Typescript is concerned. These are [detailed on the esbuild site](https://esbuild.github.io/content-types/#typescript-caveats).
646
+
647
+ ## JSX
648
+
649
+ Using JSX syntax usually requires you to manually import the JSX library you are using. For example, if you are using React, by default you will need to import React into each JSX file like this:
650
+
651
+ ```javascript
652
+ import * as React from "react";
653
+ render(<div />);
654
+ ```
655
+
656
+ This is because the JSX transform turns JSX syntax into a call to `React.createElement` but it does not itself import anything, so the React variable is not automatically present.
657
+
658
+ Proscenium generates these import statements for you. Keep in mind that this also completely changes how the JSX transform works, so it may break your code if you are using a JSX library that is not React.
659
+
660
+ In the [not too distant] future, you will be able to configure Proscenium to use a different JSX library, or to disable this auto-import completely.
661
+
662
+ ## JSON
663
+
664
+ Importing .json files parses the JSON file into a JavaScript object, and exports the object as the default export. Using it looks something like this:
665
+
666
+ ```javascript
667
+ import object from "./example.json";
668
+ console.log(object);
669
+ ```
670
+
671
+ In addition to the default export, there are also named exports for each top-level property in the JSON object. Importing a named export directly means Proscenium can automatically remove unused parts of the JSON file from the bundle, leaving only the named exports that you actually used. For example, this code will only include the version field when bundled:
672
+
673
+ ```javascript
674
+ import { version } from "./package.json";
675
+ console.log(version);
676
+ ```
677
+
678
+ ## Phlex Support
679
+
680
+ [Phlex](https://www.phlex.fun/) is a framework for building fast, reusable, testable views in pure Ruby. Proscenium works perfectly with Phlex, with support for side-loading, CSS modules, and more. Simply write your Phlex classes and inherit from `Proscenium::Phlex`.
681
+
682
+ ```ruby
683
+ class MyView < Proscenium::Phlex
684
+ def view_template
685
+ h1 { 'Hello World' }
686
+ end
687
+ end
688
+ ```
689
+
690
+ In your layouts, include `Proscenium::Phlex::AssetInclusions`, and call the `include_assets` helper.
691
+
692
+ ```ruby
693
+ class ApplicationLayout < Proscenium::Phlex
694
+ include Proscenium::Phlex::AssetInclusions # <--
695
+
696
+ def view_template(&)
697
+ doctype
698
+ html do
699
+ head do
700
+ title { 'My Awesome App' }
701
+ include_assets # <--
702
+ end
703
+ body(&)
704
+ end
705
+ end
706
+ end
707
+ ```
708
+
709
+ You can specifically include CCS and JS assets using the `include_stylesheets` and `include_javascripts` helpers, allowing you to control where they are included in the HTML.
710
+
711
+ ### Side-loading
712
+
713
+ Any Phlex class that inherits `Proscenium::Phlex` will automatically be [side-loaded](#side-loading).
714
+
715
+ ### CSS Modules
716
+
717
+ [CSS Modules](#css-modules) are fully supported in Phlex classes, with access to the [`css_module` helper](#in-your-views) if you need it. However, there is a better and more seemless way to reference CSS module classes in your Phlex classes.
718
+
719
+ Within your Phlex classes, any class names that begin with `@` will be treated as a CSS module class.
720
+
721
+ ```ruby
722
+ # /app/views/users/show_view.rb
723
+ class Users::ShowView < Proscenium::Phlex
724
+ def view_template
725
+ h1 class: :@user_name do
726
+ @user.name
727
+ end
728
+ end
729
+ end
730
+ ```
731
+
732
+ ```css
733
+ /* /app/views/users/show_view.module.css */
734
+ .userName {
735
+ color: red;
736
+ font-size: 50px;
737
+ }
738
+ ```
739
+
740
+ In the above `Users::ShowView` Phlex class, the `@user_name` class will be resolved to the `userName` class in the `users/show_view.module.css` file.
741
+
742
+ The view above will be rendered something like this:
743
+
744
+ ```html
745
+ <h1 class="user_name-ABCD1234"></h1>
746
+ ```
747
+
748
+ You can of course continue to reference regular class names in your view, and they will be passed through as is. This will allow you to mix and match CSS modules and regular CSS classes in your views.
749
+
750
+ ```ruby
751
+ # /app/views/users/show_view.rb
752
+ class Users::ShowView < Proscenium::Phlex
753
+ def view_template
754
+ h1 class: :[@user_name, :title] do
755
+ @user.name
756
+ end
757
+ end
758
+ end
759
+ ```
760
+
761
+ ```html
762
+ <h1 class="user_name-ABCD1234 title">Joel Moss</h1>
763
+ ```
764
+
765
+ ## ViewComponent Support
766
+
767
+ [ViewComponent](https://viewcomponent.org/) iA framework for creating reusable, testable & encapsulated view components, built to integrate seamlessly with Ruby on Rails. Proscenium works perfectly with ViewComponent, with support for side-loading, CSS modules, and more. Simply write your ViewComponent classes and inherit from `Proscenium::ViewComponent`.
768
+
769
+ ```ruby
770
+ class MyView < Proscenium::ViewComponent
771
+ def call
772
+ tag.h1 'Hello World'
773
+ end
774
+ end
775
+ ```
776
+
777
+ ### Side-loading
778
+
779
+ Any ViewComponent class that inherits `Proscenium::ViewComponent` will automatically be [side-loaded](#side-loading).
780
+
781
+ ### CSS Modules
782
+
783
+ [CSS Modules](#css-modules) are fully supported in ViewComponent classes, with access to the [`css_module` helper](#in-your-views) if you need it.
784
+
785
+ ```ruby
786
+ # /app/components/user_component.rb
787
+ class UserComponent < Proscenium::ViewComponent
788
+ def view_template
789
+ div.h1 @user.name, class: css_module(:user_name)
790
+ end
791
+ end
792
+ ```
793
+
794
+ ```css
795
+ /* # /app/components/user_component.module.css */
796
+ .userName {
797
+ color: red;
798
+ font-size: 50px;
799
+ }
800
+ ```
801
+
802
+ The view above will be rendered something like this:
803
+
804
+ ```html
805
+ <h1 class="user_name-ABCD1234">Joel Moss</h1>
806
+ ```
807
+
808
+ ## Cache Busting
809
+
810
+ > _COMING SOON_
811
+
812
+ 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.
813
+
814
+ For example, if you set `REVISION=v1`, URL's will be appended with `?v1`: `/my/imported/file.js?v1`.
815
+
816
+ It is assumed that the `REVISION` env var will be unique between deploys. If it isn't, then assets will continue to be cached as the same version between deploys. I recommend you assign a version number or to use the Git commit hash of the deploy. Just make sure it is unique for each deploy.
817
+
818
+ You can set the `cache_query_string` config option directly to define any query string you wish:
819
+
820
+ ```ruby
821
+ Rails.application.config.proscenium.cache_query_string = 'my-cache-busting-version-string'
822
+ ```
823
+
824
+ The cache is set with a `max-age` of 30 days. You can customise this with the `cache_max_age` config option:
825
+
826
+ ```ruby
827
+ Rails.application.config.proscenium.cache_max_age = 12.months.to_i
828
+ ```
829
+
830
+ ## rjs is back
831
+
832
+ Proscenium brings back RJS! Any path ending in .rjs will be served from your Rails app. This allows you to import server rendered javascript.
833
+
834
+ ## Resolution
835
+
836
+ Proscenium will serve files ending with any of these extension: `js,mjs,ts,css,jsx,tsx` from the following directories, and their sub-directories of your Rails application's root: `/app`, `/lib`, `/config`, `/node_modules`, `/vendor`.
837
+
838
+ So a file at `/app/views/users/index.js` will be served from `https://yourapp.com/app/views/users/index.js`.
839
+
840
+ You can continue to access any file in the `/public` directory as you normally would. Proscenium will not process files in the `/public` directory.
841
+
842
+ If requesting a file that exists in a root directory and the public directory, the file in the public directory will be served. For example, if you have a file at `/lib/foo.js` and `/public/lib/foo.js`, and you request `/lib/foo.js`, the file in the public directory (`/public/lib/foo.js`) will be served.
843
+
844
+ ### Assets from Rails Engines
845
+
846
+ Proscenium can serve assets from Rails Engines that are installed in your Rails app.
847
+
848
+ An engine that wants to expose its assets via Proscenium to the application must add Proscenium as a dependency, and add itself to the list of engines in the Proscenium config options `Proscenium.config.engines`.
849
+
850
+ For example, we have a gem called `gem1` that has Proscenium as a dependency, and exposes a Rails engine. It has some assets that it wants to expose to the application. To do this, it adds itself to the list of engines in the Proscenium config `engines` option:
851
+
852
+ ```ruby
853
+ class Gem1::Engine < ::Rails::Engine
854
+ config.proscenium.engines << self
855
+ end
856
+ ```
857
+
858
+ When this gem is installed in any Rails application, its assets will be available at the URL `/gem1/...`. For example, if the gem has a file `lib/styles.css`, it can be requested at `/gem1/lib/styles.css`.
859
+
860
+ The same directories and file extensions are supported as for the application itself.
861
+
862
+ It is important to note that the application takes precedence over the gem. So if the application has a file at `/public/gem1/lib/styles.css`, and the gem also has a file at `/lib/styles.css`, then the file in the application will be served. This is because both files would be accessible at the same URL: `/gem1/lib/styles.css`.
863
+
864
+ ## Thanks
865
+
866
+ 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.
867
+
868
+ 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.
869
+
870
+ ## Development
871
+
872
+ Before doing anything else, you will need compile a local version of the Go binary. This is because the Go binary is not checked into the repo. To compile the binary, run:
873
+
874
+ ```bash
875
+ bundle exec rake compile:local
876
+ ```
877
+
878
+ ### Running tests
879
+
880
+ We have tests for both Ruby and Go. To run the Ruby tests:
881
+
882
+ ```bash
883
+ bundle exec sus
884
+ ```
885
+
886
+ To run the Go tests:
887
+
888
+ ```bash
889
+ go test ./test
890
+ ```
891
+
892
+ ### Running Go benchmarks
893
+
894
+ ```bash
895
+ go test ./internal/builder -bench=. -run="^$" -count=10 -benchmem
896
+ ```
897
+
898
+ ## Contributing
899
+
900
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/joelmoss/proscenium>. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/joelmoss/proscenium/blob/master/CODE_OF_CONDUCT.md).
901
+
902
+ ## License
903
+
904
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
905
+
906
+ ## Code of Conduct
907
+
908
+ Everyone interacting in the Proscenium project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/joelmoss/proscenium/blob/master/CODE_OF_CONDUCT.md).