proscenium 0.26.0-x86_64-linux-gnu

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +84 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +1020 -0
  5. data/exe/proscenium +19 -0
  6. data/exe/psm +5 -0
  7. data/lib/generators/proscenium/bun/bun_generator.rb +150 -0
  8. data/lib/generators/proscenium/bun/templates/proscenium.preload.js +61 -0
  9. data/lib/proscenium/app_package.rb +90 -0
  10. data/lib/proscenium/builder.rb +403 -0
  11. data/lib/proscenium/bundled_gems.rb +140 -0
  12. data/lib/proscenium/cli/brackets.rb +66 -0
  13. data/lib/proscenium/cli/bun_linker.rb +79 -0
  14. data/lib/proscenium/cli/capabilities.json +29 -0
  15. data/lib/proscenium/cli/collisions.rb +94 -0
  16. data/lib/proscenium/cli/contexts.rb +133 -0
  17. data/lib/proscenium/cli/diff.rb +80 -0
  18. data/lib/proscenium/cli/error.rb +291 -0
  19. data/lib/proscenium/cli/gem_archive.rb +44 -0
  20. data/lib/proscenium/cli/gem_check.rb +150 -0
  21. data/lib/proscenium/cli/gitignore.rb +63 -0
  22. data/lib/proscenium/cli/inspect.rb +257 -0
  23. data/lib/proscenium/cli/install.rb +378 -0
  24. data/lib/proscenium/cli/manager.rb +323 -0
  25. data/lib/proscenium/cli/platform_variants.rb +82 -0
  26. data/lib/proscenium/cli/pnpm_approvals.rb +109 -0
  27. data/lib/proscenium/cli/pnpm_workspace.rb +149 -0
  28. data/lib/proscenium/cli/project_lock.rb +106 -0
  29. data/lib/proscenium/cli/registration.rb +140 -0
  30. data/lib/proscenium/cli/reporter.rb +103 -0
  31. data/lib/proscenium/cli/rules.rb +169 -0
  32. data/lib/proscenium/cli/runner.rb +142 -0
  33. data/lib/proscenium/cli/verify.rb +107 -0
  34. data/lib/proscenium/cli.rb +122 -0
  35. data/lib/proscenium/context_map.rb +181 -0
  36. data/lib/proscenium/css_module/path.rb +32 -0
  37. data/lib/proscenium/css_module/transformer.rb +118 -0
  38. data/lib/proscenium/css_module.rb +63 -0
  39. data/lib/proscenium/dependency_context.rb +74 -0
  40. data/lib/proscenium/ensure_loaded.rb +31 -0
  41. data/lib/proscenium/error.rb +10 -0
  42. data/lib/proscenium/ext/proscenium +0 -0
  43. data/lib/proscenium/ext/proscenium.h +113 -0
  44. data/lib/proscenium/helper.rb +82 -0
  45. data/lib/proscenium/importer.rb +189 -0
  46. data/lib/proscenium/log_subscriber.rb +38 -0
  47. data/lib/proscenium/manifest.rb +48 -0
  48. data/lib/proscenium/mapping_generation.rb +64 -0
  49. data/lib/proscenium/middleware/base.rb +102 -0
  50. data/lib/proscenium/middleware/chunks.rb +46 -0
  51. data/lib/proscenium/middleware/esbuild.rb +11 -0
  52. data/lib/proscenium/middleware/ruby_gems.rb +37 -0
  53. data/lib/proscenium/middleware/silence_request.rb +31 -0
  54. data/lib/proscenium/middleware/vendor.rb +31 -0
  55. data/lib/proscenium/middleware.rb +67 -0
  56. data/lib/proscenium/monkey.rb +77 -0
  57. data/lib/proscenium/railtie.rb +106 -0
  58. data/lib/proscenium/railties/assets.rake +19 -0
  59. data/lib/proscenium/resolver.rb +67 -0
  60. data/lib/proscenium/runtime/bootstrap.js +266 -0
  61. data/lib/proscenium/runtime/bun.js +143 -0
  62. data/lib/proscenium/runtime/server.rb +702 -0
  63. data/lib/proscenium/side_load.rb +210 -0
  64. data/lib/proscenium/source_path.rb +15 -0
  65. data/lib/proscenium/stale_contexts.rb +118 -0
  66. data/lib/proscenium/templates/rescues/build_error.html.erb +32 -0
  67. data/lib/proscenium/utils.rb +104 -0
  68. data/lib/proscenium/version.rb +5 -0
  69. data/lib/proscenium/yaml_tree.rb +48 -0
  70. data/lib/proscenium.rb +87 -0
  71. metadata +148 -0
data/README.md ADDED
@@ -0,0 +1,1020 @@
1
+ # Proscenium - Integrated Frontend Development for Rails
2
+
3
+ > 🗣️ prow · see · nee · uhm
4
+ >
5
+ > _noun_: **proscenium**
6
+ >
7
+ > - _the part of a theatre stage in front of the curtain._
8
+
9
+ **_Proscenium_** treats your frontend and client-side code as first class citizens of your Rails app, and assumes a "fast by default" internet. It bundles and minifies JavaScript (+JSX), TypeScript (+TSX) and CSS in real time, on demand, and with zero configuration.
10
+
11
+ **The highlights:**
12
+
13
+ - Fast, real-time bundling, tree-shaking, code-splitting and minification of Javascript (.js,.jsx), Typescript (.ts,.tsx) and CSS (.css).
14
+ - NO JavaScript runtime needed (eg. Node) - just the browser!
15
+ - NO build step or pre-compilation.
16
+ - NO additional process or server - Just run `rails server`!
17
+ - Transforms newer JavaScript and CSS syntax to older syntax for older browsers.
18
+ - Deep integration with Rails.
19
+ - Automatically side-load JS and CSS for your layouts, views, and partials.
20
+ - Import from NPM, URL's, and locally.
21
+ - CSS Modules & mixins.
22
+ - Source maps.
23
+
24
+ ## Table of Contents
25
+
26
+ - [Getting Started](#getting-started)
27
+ - [Installation](#installation)
28
+ - [Supported platforms](#supported-platforms)
29
+ - [Client-Side Code Anywhere](#client-side-code-anywhere)
30
+ - [Side Loading](#side-loading)
31
+ - [Bundling](#bundling)
32
+ - [Import from NPM (`node_modules`)](#import-from-npm-node_modules)
33
+ - [RubyGem NPM dependencies](#rubygem-npm-dependencies)
34
+ - [Local Imports](#local-imports)
35
+ - [Unbundling](#unbundling)
36
+ - [Source Maps](#source-maps)
37
+ - [SVG](#svg)
38
+ - [Environment Variables](#environment-variables)
39
+ - [i18n](#i18n)
40
+ - [JavaScript](#javascript)
41
+ - [Tree Shaking](#tree-shaking)
42
+ - [Code Splitting](#code-splitting)
43
+ - [`__filename` and `__dirname`](#__filename-and-__dirname)
44
+ - [JavaScript Caveats](#javascript-caveats)
45
+ - [CSS](#css)
46
+ - [Importing CSS from JavaScript](#importing-css-from-javascript)
47
+ - [CSS Modules](#css-modules)
48
+ - [CSS Mixins](#css-mixins)
49
+ - [CSS Caveats](#css-caveats)
50
+ - [Typescript](#typescript)
51
+ - [Typescript Caveats](#typescript-caveats)
52
+ - [JSX](#jsx)
53
+ - [JSON](#json)
54
+ - [rjs is back!](#rjs-is-back)
55
+ - [Testing your JavaScript](#testing-your-javascript)
56
+ - [Resolution](#resolution)
57
+ - [Aliases](#aliases)
58
+ - [Pre-compilation](#pre-compilation)
59
+ - [Puma `preload_app!` and Cluster Mode](#puma-preload_app-and-cluster-mode)
60
+ - [Thanks](#thanks)
61
+ - [Development](#development)
62
+
63
+ ## Getting Started
64
+
65
+ 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:
66
+
67
+ - [Getting Started with a new Rails app](docs/guides/new_rails_app.md)
68
+ - Getting Started with an existing Rails app
69
+ - [Migrate from Sprockets](docs/guides/migrate_from_sprockets.md)
70
+ - [RubyGem NPM dependencies](docs/guides/rubygem_npm_dependencies.md)
71
+ - [Shipping NPM dependencies in a gem](docs/guides/gem_author.md)
72
+
73
+ ## Installation
74
+
75
+ Add this line to your Rails application's Gemfile, and you're good to go:
76
+
77
+ ```ruby
78
+ gem 'proscenium'
79
+ ```
80
+
81
+ Please note that Proscenium is designed solely for use with Rails. It needs Ruby 3.4 or newer, and
82
+ Rails 7.2 to 8.x.
83
+
84
+ Now if you start your Rails app, you can open any front end code (JS, CSS, etc.). For example, a file at `app/assets/stylesheets/application.css` can be accessed at `https://localhost:3000/app/assets/stylesheets/application.css`, which will be transformed, bundled, and minified [in production] in real time.
85
+
86
+ ### Supported platforms
87
+
88
+ Proscenium is a Ruby gem wrapped around a Go library, so it ships a precompiled gem per platform.
89
+ Bundler picks the right one for you.
90
+
91
+ | Platform | Gem |
92
+ | --- | --- |
93
+ | macOS, Apple silicon | `arm64-darwin` |
94
+ | macOS, Intel | `x86_64-darwin` |
95
+ | Linux glibc, arm64 | `aarch64-linux-gnu` |
96
+ | Linux glibc, x86-64 | `x86_64-linux-gnu` |
97
+ | Windows, x64 ([RubyInstaller](https://rubyinstaller.org/)) | `x64-mingw-ucrt` |
98
+
99
+ The Linux gems need **RubyGems 3.3.22 or newer**, which is the first version able to tell
100
+ `-gnu` from `-musl`. Older versions cannot, so they refuse the gem rather than installing one your
101
+ machine cannot load. Ruby 3.4 already ships a newer RubyGems than this, so you are almost certainly
102
+ fine; if not, `gem update --system`. Bundler 2.5.6 or newer resolves the platform names without
103
+ needing anything else from you.
104
+
105
+ Installing on a platform not listed here gets you the platform-less gem, which carries no compiled
106
+ library, and Proscenium will say so by name when it loads rather than failing somewhere inside FFI.
107
+ If your platform should be supported, please [open an
108
+ issue](https://github.com/joelmoss/proscenium/issues).
109
+
110
+ The Linux gems name their libc. Earlier releases shipped bare `x86_64-linux` and `aarch64-linux`
111
+ names, which RubyGems matches on glibc and musl alike. If your `Gemfile.lock` is pinned to either,
112
+ add the matching `-gnu` platform:
113
+
114
+ ```sh
115
+ bundle lock --add-platform x86_64-linux-gnu # or aarch64-linux-gnu, on ARM
116
+ ```
117
+
118
+ Adding it is enough. There is no need to remove the old platform.
119
+
120
+ On Windows, with a `Gemfile.lock` written on another machine: a plain `bundle install` adds the
121
+ platform itself, but in frozen or deployment mode Bundler will not, so add it once.
122
+
123
+ ```sh
124
+ bundle lock --add-platform x64-mingw-ucrt
125
+ ```
126
+
127
+ ### Alpine and other musl Linux distributions are not supported
128
+
129
+ Use a glibc base image, such as `ruby:3.4-slim` rather than `ruby:3.4-alpine`.
130
+
131
+ This is not an oversight. Proscenium's Go library is built with `-buildmode=c-shared` and loaded
132
+ at runtime through Ruby's FFI, which uses `dlopen`. Go emits initial-exec TLS relocations into
133
+ such libraries, and musl refuses to `dlopen` anything carrying them, by design:
134
+
135
+ ```
136
+ Error relocating ...: free: initial-exec TLS resolves to dynamic definition
137
+ ```
138
+
139
+ The library builds and links against musl perfectly well; it just cannot be loaded the way
140
+ Proscenium needs to load it. This is [golang/go#54805](https://github.com/golang/go/issues/54805),
141
+ open since 2022, and the linker flag that resolves it has not shipped as of Go 1.27. Musl gems
142
+ will follow when it does.
143
+
144
+ ## Client-Side Code Anywhere
145
+
146
+ 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, you can simply put them wherever you want within your app, and just run Rails!
147
+
148
+ 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`.
149
+
150
+ Simply put your JS(X) and CSS anywhere you want, and they will be served by your Rails app from the same location where you placed them.
151
+
152
+ Using the examples above...
153
+
154
+ - `app/views/users/index.js` => `https://localhost:3000/app/views/users/index.js`
155
+ - `app/views/layouts/application.css` => `https://localhost:3000/app/views/layouts/application.css`
156
+ - `lib/utils.js` => `https://localhost:3000/lib/utils.js`
157
+ - `app/components/menu_component.jsx` => `https://localhost:3000/app/components/menu_component.jsx`
158
+ - `config/properties.css` => `https://localhost:3000/config/properties.css`
159
+
160
+ ## Side Loading
161
+
162
+ Proscenium is best experienced when your assets are automatically side loaded.
163
+
164
+ ### The Problem
165
+
166
+ With Rails you would typically load your JavaScript and CSS assets declaratively using the `javascript_include_tag` and `stylesheet_link_tag` helpers.
167
+
168
+ For example, you may have top-level "application" styles located in a file at `/app/assets/stylesheets/application.css`. Likewise, you may have some global JavaScript located in a file at `/app/javascript/application.js`.
169
+
170
+ You would manually and declaratively include those two files in each of your layouts, something like this:
171
+
172
+ ```erb
173
+ <%# /app/views/layouts/application.html.erb %>
174
+
175
+ <!DOCTYPE html>
176
+ <html>
177
+ <head>
178
+ <title>Hello World</title>
179
+ <%= stylesheet_link_tag 'application' %> <!-- << Your app CSS -->
180
+ </head>
181
+ <body>
182
+ <%= yield %>
183
+ <%= javascript_include_tag 'application' %> <!-- << Your app JS -->
184
+ </body>
185
+ </html>
186
+ ```
187
+
188
+ 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:
189
+
190
+ ```erb
191
+ <%# /app/views/users/index.html.erb %>
192
+
193
+ <%= stylesheet_link_tag 'users' %>
194
+ <%= javascript_include_tag 'users' %>
195
+
196
+ <%# needed by the `users/_user.html.erb` partial %>
197
+ <%= javascript_include_tag '_user' %>
198
+
199
+ <% render @users %>
200
+ ```
201
+
202
+ 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.
203
+
204
+ ### The Solution
205
+
206
+ 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.
207
+
208
+ 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 and CSS file at `app/views/users/index.js` (or TypeScript with a .ts extension) and `app/views/users/index.css`. If it finds one, it will automatically include it in the HTML for that view. And only for that view.
209
+
210
+ This allows you to keep your assets organized alongside the views, partials, and components that use them, without having to manually track and include them. It also means only the assets that are needed are included.
211
+
212
+ JSX is also supported for JavaScript and Typescript. Simply use the `.jsx` or `.tsx` extension instead of `.js` or `.ts`.
213
+
214
+ ### Usage
215
+
216
+ Simply create a JS and/or CSS file with the same name as any view, partial or layout.
217
+
218
+ Let's continue with our problem example above, where we have the following assets
219
+
220
+ - `/app/assets/application.css`
221
+ - `/app/assets/application.js`
222
+ - `/app/assets/users.css`
223
+ - `/app/assets/users.js`
224
+ - `/app/assets/user.js`
225
+
226
+ Your application layout is at `/app/views/layouts/application.html.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:
227
+
228
+ - `/app/views/layouts/application.css`
229
+ - `/app/views/layouts/application.js`
230
+ - `/app/views/users/index.css`
231
+ - `/app/views/users/index.js`
232
+ - `/app/views/users/_user.js` (partial)
233
+
234
+ 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:
235
+
236
+ ```erb
237
+ <!DOCTYPE html>
238
+ <html>
239
+ <head>
240
+ <title>Hello World</title>
241
+ <%= include_assets # <-- %>
242
+ </head>
243
+ <body>
244
+ <%= yield %>
245
+ </body>
246
+ </html>
247
+ ```
248
+
249
+ On each page request, Proscenium will check if any of your views, layouts and partials have a JS/TS/CSS file of the same name, and then include them wherever you placed the `include_assets` helper.
250
+
251
+ 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.
252
+
253
+ Side loading is enabled by default, but you can disable it by setting `config.proscenium.side_load` to `false` in your `/config/application.rb`.
254
+
255
+ There are also `include_stylesheets` and `include_javascripts` helpers to allow you to control where the CSS and JS assets are included in the HTML. These helpers should be used instead of `include_assets` if you want to control exactly where the assets are included.
256
+
257
+ ### Controlling side loading
258
+
259
+ `sideload_assets` turns side loading off, or sets attributes on the tags it adds. In a controller, it applies to every view, layout and partial that controller renders. Each of these is an alternative:
260
+
261
+ ```ruby
262
+ class UsersController < ApplicationController
263
+ sideload_assets false # side load nothing
264
+ sideload_assets css: false # side load JS only
265
+ sideload_assets js: { defer: true } # add attributes to each script tag
266
+ sideload_assets proc { !request.xhr? } # evaluated against the controller, on every request
267
+ end
268
+ ```
269
+
270
+ In a view, layout or partial, it applies to that template, for that render only:
271
+
272
+ ```erb
273
+ <% sideload_assets false %>
274
+ ```
275
+
276
+ Call it outside any `cache` block. A cache hit skips the block, and the call with it, so that render side loads the template's assets as though it had not been called.
277
+
278
+ A partial rendered as a collection is side loaded once for the whole collection, so a `sideload_assets` call in any item applies to every item. With `cached: true`, a cache hit renders no items, so the call never runs. To control a cached collection partial, call `sideload_assets` in the controller instead.
279
+
280
+ In a partial rendered with a block, as in `render 'box' do ... end`, a call in the partial applies to the partial, and a call inside the block applies to the template that passed it.
281
+
282
+ ## Bundling
283
+
284
+ To bundle a file means to inline any imported dependencies into the file itself. This process is recursive so dependencies of dependencies (and so on) will also be inlined.
285
+
286
+ Proscenium will bundle by default, and in real time. So there is no separate build step or pre-compilation.
287
+
288
+ Proscenium supports importing JS, JSX, TS, TSX, CSS and SVG from NPM, by URL, your local app, and even from other Ruby Gems.
289
+
290
+ Both static ([`import`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)) and dynamic ([`import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)) imports are supported for JavaScript and TypeScript, and can be used to import JS, TS, JSX, TSX, JSON, CSS and SVG files.
291
+
292
+ The [`@import`](https://developer.mozilla.org/en-US/docs/Web/CSS/@import) CSS at-rule is supported for CSS.
293
+
294
+ ### Non-analyzable imports
295
+
296
+ Import paths are currently only bundled if they are a string literal or a glob pattern. Other forms of import paths are not bundled, and are instead preserved verbatim in the generated output. This is because bundling is a compile-time operation and Proscenium doesn't support all forms of run-time path resolution.
297
+
298
+ Here are some examples:
299
+
300
+ ```js
301
+ // Analyzable imports (will be bundled)
302
+ import "pkg";
303
+ import("pkg");
304
+ import(`./locale-${foo}.json`);
305
+
306
+ // Non-analyzable imports (will not be bundled)
307
+ import(`pkg/${foo}`);
308
+ ```
309
+
310
+ The way to work around non-analyzable imports is to mark the package containing this problematic code as [unbundled](#unbundling) so that it's not included in the bundle. You will then need to ensure that a copy of the external package is available to your bundled code at run-time.
311
+
312
+ ### Import from NPM (`node_modules`)
313
+
314
+ 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 located at the root of your Rails app.
315
+
316
+ Install the package you want to import using your package manager of choice...
317
+
318
+ ```
319
+ npm install react
320
+ ```
321
+
322
+ ...and then import it as you would any other package.
323
+
324
+ ```js
325
+ import React from "react";
326
+ ```
327
+
328
+ ### RubyGem NPM dependencies
329
+
330
+ A gem can ship frontend code that imports npm packages, declared in a package.json of its own. Add
331
+ the gem with Bundler, then install its NPM dependencies with your app's package manager,
332
+ pnpm or Bun:
333
+
334
+ ```bash
335
+ bundle exec proscenium install
336
+ ```
337
+
338
+ That writes a small package.json for each such gem under `.proscenium/packages/`, holding only the
339
+ gem's dependencies, registers them with your package manager, and runs its install. Commit what it
340
+ lists. The gem's own files stay where Bundler put them. Run it again after every Gemfile change, and
341
+ in CI run `bundle exec proscenium install --frozen`, which changes nothing you commit and fails if
342
+ anything is out of date. When something is not working, `bundle exec proscenium doctor` (also
343
+ `inspect`) lists every problem it finds, each with its fix. `psm` is a shorter name for the
344
+ same command: `bundle exec psm install`.
345
+
346
+ Its install covers your app's own packages too, so it replaces `pnpm install` or `bun install`
347
+ everywhere you ran them, and `install --frozen` replaces a frozen install. It does not add, remove
348
+ or upgrade packages: for your app's own dependencies, keep using `pnpm add`, `pnpm remove` and
349
+ `pnpm update`, or `bun add`, `bun remove` and `bun update`.
350
+
351
+ See [RubyGem NPM dependencies](docs/guides/rubygem_npm_dependencies.md) for the details,
352
+ and the [gem author guide](docs/guides/gem_author.md) if you are writing such a gem.
353
+
354
+ ### Local Imports
355
+
356
+ And of course you can import your own code, using relative or absolute paths (file extension is optional, and absolute paths use your Rails root as the base):
357
+
358
+ ```js
359
+ import utils from "/lib/utils";
360
+ import constants from "./constants";
361
+ import Header from "/app/components/header";
362
+ ```
363
+
364
+ ```css
365
+ @import "/lib/reset";
366
+ ```
367
+
368
+ ### Unbundling
369
+
370
+ Sometimes you don't want to bundle an import. For example, you want to ensure that only one instance of React is loaded. In these cases, you can use the `unbundle` import attribute:
371
+
372
+ ```js
373
+ import React from "react" with { unbundle: 'true' };
374
+ ```
375
+
376
+ You can also unbundle entries in [`aliases`](#aliases) using an `unbundle:` prefix, which ensures that all imports of a particular path are always unbundled:
377
+
378
+ ```ruby
379
+ config.proscenium.aliases = {
380
+ "react": "unbundle:react"
381
+ }
382
+ ```
383
+
384
+ Then just import as normal:
385
+
386
+ ```js
387
+ import React from "react";
388
+ ```
389
+
390
+ Or if you don't want any bundling at all, simply turn it off application-wide:
391
+
392
+ ```ruby
393
+ config.proscenium.bundle = false
394
+ ```
395
+
396
+ This will mean every asset and import will be loaded independently.
397
+
398
+ ## Source Maps
399
+
400
+ 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.
401
+
402
+ Source map output is supported for both JavaScript and CSS. Each file is appended with the link to the source map. For example:
403
+
404
+ ```js
405
+ //# sourceMappingURL=/app/views/layouts/application.js.map
406
+ ```
407
+
408
+ Your browsers dev tools should pick this up and automatically load the source map when and where needed.
409
+
410
+ ## SVG
411
+
412
+ 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.
413
+
414
+ That component is compiled with esbuild's default JSX runtime, React, even when a `tsconfig.json` or `jsconfig.json` names another `jsxImportSource`, so importing an SVG from JSX needs `react` installed.
415
+
416
+ When bundling, the SVG is read as markup, never as code, whether it is local or imported by URL. Its text and attribute values are kept as plain strings, so an expression such as `{proscenium.env.API_KEY}` inside an SVG stays literal text, and anything after the root element is ignored. The SVG is still rendered into the page as-is, so only import SVGs from sources you trust.
417
+
418
+ ## Environment Variables
419
+
420
+ You can define and access any environment variable from your JavaScript and Typescript under the `proscenium.env` namespace.
421
+
422
+ For performance and security reasons you must declare the environment variable names that you wish to expose in your `config/application.rb` file.
423
+
424
+ ```ruby
425
+ config.proscenium.env_vars = Set['APP_VERSION', 'PUBLIC_API_URL']
426
+ config.proscenium.env_vars << 'SENTRY_DSN'
427
+ ```
428
+
429
+ > [!WARNING]
430
+ > A declared variable's value is written as a plain string into any JavaScript that references it, both in what Proscenium serves and in what `assets:precompile` writes under `public/`. Anyone who loads the page can read it. Never declare a secret, such as an API key, password or token.
431
+
432
+ 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.
433
+
434
+ These declared environment variables will be replaced with constant expressions, allowing you to use this like this:
435
+
436
+ ```js
437
+ console.log(proscenium.env.RAILS_ENV); // console.log("development")
438
+ console.log(proscenium.env.RAILS_ENV === "development"); // console.log(true)
439
+ ```
440
+
441
+ The `RAILS_ENV` and `NODE_ENV` environment variables will always automatically be declared for you.
442
+
443
+ 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.
444
+
445
+ Environment variables are particularly powerful in aiding [tree shaking](#tree-shaking).
446
+
447
+ ```js
448
+ function start() {
449
+ console.log("start");
450
+ }
451
+ function doSomethingDangerous() {
452
+ console.log("resetDatabase");
453
+ }
454
+
455
+ proscenium.env.RAILS_ENV === "development" && doSomethingDangerous();
456
+
457
+ start();
458
+ ```
459
+
460
+ In any environment other than development, such as production, the above code will be transformed into the following code, discarding the definition of, and call to, `doSomethingDangerous()`.
461
+
462
+ ```js
463
+ function start() {
464
+ console.log("start");
465
+ }
466
+ start();
467
+ ```
468
+
469
+ Please note that for security reasons environment variables are not replaced in URL imports.
470
+
471
+ An undefined environment variable will be replaced with `undefined`.
472
+
473
+ ```js
474
+ console.log(proscenium.env.UNKNOWN); // console.log((void 0).UNKNOWN)
475
+ ```
476
+
477
+ 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):
478
+
479
+ ```js
480
+ if (typeof proscenium.env?.UNKNOWN !== "undefined") {
481
+ // do something if UNKNOWN is defined
482
+ }
483
+ ```
484
+
485
+ ## i18n
486
+
487
+ Support is provided for importing your Rails locale files from `config/locales/*.yml`, exporting them as JSON.
488
+
489
+ ```js
490
+ import translations from "proscenium/i18n";
491
+ // translations.en.*
492
+ ```
493
+
494
+ If you have multiple locale files, they will be merged together. into one json object.
495
+
496
+ An app with no `config/locales` directory exports an empty object. A directory that exists but
497
+ cannot be read - wrong permissions, an I/O error - fails the build instead, because the alternative
498
+ is an app that silently ships with every translation missing.
499
+
500
+ Note that because it is assumed that you will be consuming these translations in the browser, all keys are converted to camelCase, as per the JavaScript conventions.
501
+
502
+ ## Javascript
503
+
504
+ By default, Proscenium's output will take advantage of all modern JS features from the ES2022 spec and earlier. 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. Any syntax feature that is not supported by ES2020 will be transformed into older JavaScript syntax that is more widely supported.
505
+
506
+ ### Tree Shaking
507
+
508
+ 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.
509
+
510
+ ```javascript
511
+ function one() {
512
+ console.log("one");
513
+ }
514
+ function two() {
515
+ console.log("two");
516
+ }
517
+ one();
518
+ ```
519
+
520
+ The above code will be transformed to the following code, discarding `two()`, as it is never called.
521
+
522
+ ```javascript
523
+ function one() {
524
+ console.log("one");
525
+ }
526
+ one();
527
+ ```
528
+
529
+ ### Code Splitting
530
+
531
+ [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.
532
+
533
+ As an example:
534
+
535
+ ```js
536
+ // /lib/son.js
537
+ import father from "./father";
538
+
539
+ father() + " and Son";
540
+ ```
541
+
542
+ ```js
543
+ // /lib/daughter.js
544
+ import father from "./father";
545
+
546
+ father() + " and Daughter";
547
+ ```
548
+
549
+ ```js
550
+ // /lib/father.js
551
+ export default () => "Father";
552
+ ```
553
+
554
+ 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.
555
+
556
+ If these files are side loaded, then `father.js` will be split off into a separate file or chunk, and only downloaded once.
557
+
558
+ - 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.
559
+
560
+ - 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.
561
+
562
+ - 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.
563
+
564
+ 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`:
565
+
566
+ ```ruby
567
+ config.proscenium.code_splitting = false
568
+ ```
569
+
570
+ ### `__filename` and `__dirname`
571
+
572
+ Proscenium provides Node.js-style `__filename` and `__dirname` constants in your JavaScript and TypeScript files. These are replaced at build time with the root-relative path of the current file and its directory respectively.
573
+
574
+ ```js
575
+ // /app/views/users/index.js
576
+ console.log(__filename); // "/app/views/users/index.js"
577
+ console.log(__dirname); // "/app/views/users"
578
+ ```
579
+
580
+ Files inside Ruby gems resolve to their `@rubygems/` scoped path:
581
+
582
+ ```js
583
+ // Inside the "mygem" gem
584
+ console.log(__filename); // "@rubygems/mygem/lib/mygem/component.js"
585
+ console.log(__dirname); // "@rubygems/mygem/lib/mygem"
586
+ ```
587
+
588
+ Note that `__filename` and `__dirname` are not injected into files within `node_modules`.
589
+
590
+ ### JavaScript Caveats
591
+
592
+ 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).
593
+
594
+ ## CSS
595
+
596
+ 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.
597
+
598
+ 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).
599
+
600
+ The new CSS nesting syntax is supported, and transformed into non-nested CSS for older browsers.
601
+
602
+ Proscenium will also automatically insert vendor prefixes so that your CSS will work in older browsers.
603
+
604
+ ### Importing CSS from JavaScript
605
+
606
+ You can also import CSS from JavaScript. When you do this, Proscenium will automatically append each stylesheet to the document's head as a `<style>` element.
607
+
608
+ If your page includes a `<meta name="csp-nonce" content="...">` tag, the injected `<style>` elements will automatically pick up the nonce value for Content Security Policy compliance.
609
+
610
+ ```jsx
611
+ import "./button.css";
612
+
613
+ export let Button = ({ text }) => {
614
+ return <div className="button">{text}</div>;
615
+ };
616
+ ```
617
+
618
+ ### CSS Modules
619
+
620
+ 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.)
621
+
622
+ 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.
623
+
624
+ ```css
625
+ .title {
626
+ font-size: 20em;
627
+ }
628
+ ```
629
+
630
+ The above input produces:
631
+
632
+ ```css
633
+ .title-5564cdbb {
634
+ font-size: 20em;
635
+ }
636
+ ```
637
+
638
+ You now have a unique class name that you can use pretty much anywhere.
639
+
640
+ #### In your Views
641
+
642
+ 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.
643
+
644
+ With [side-loading](#side-loading) setup, you can use the `css_module` helper as follows.
645
+
646
+ ```erb
647
+ <div>
648
+ <h1 class="<%= css_module :hello_title %>">Hello World</h1>
649
+ <p class="<%= css_module :body, :paragraph %>">
650
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
651
+ </p>
652
+ </div>
653
+ ```
654
+
655
+ `css_module` accepts multiple class names, and will return a space-separated string of transformed CSS module names. Arrays of names are flattened, and `nil`, `false` and blank names are ignored, so `css_module :card, (active? && :active)` works.
656
+
657
+ ```ruby
658
+ css_module :my_module_name
659
+ # => "my_module_name-ABCD1234"
660
+ ```
661
+
662
+ 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.
663
+
664
+ ```ruby
665
+ css_module '/app/components/button.css@big_button'
666
+ # => "big_button"
667
+ ```
668
+
669
+ It also supports NPM packages (already installed in /node_modules):
670
+
671
+ ```ruby
672
+ css_module 'mypackage/button@big_button'
673
+ # => "big_button"
674
+ ```
675
+
676
+ `css_module` also accepts a `path` keyword argument, which allows you to specify the path to the CSS
677
+ file. Note that this will use the given path for all class names passed to that instance of `css_module`.
678
+
679
+ ```ruby
680
+ css_module :my_module_name, path: Rails.root.join('app/components/button.css')
681
+ ```
682
+
683
+ #### In your JavaScript
684
+
685
+ 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.
686
+
687
+ ```js
688
+ import styles from "./styles.module.css";
689
+ // styles == { header: 'header-5564cdbb' }
690
+ ```
691
+
692
+ 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.
693
+
694
+ Also, importing a CSS module into another CSS module will result in the same digest string for all classes.
695
+
696
+ ### CSS Mixins
697
+
698
+ 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.
699
+
700
+ CSS mixins are supported using the `@define-mixin` and `@mixin` at-rules.
701
+
702
+ 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:
703
+
704
+ ```css
705
+ // /lib/mixins.css
706
+ @define-mixin bigText {
707
+ font-size: 50px;
708
+ }
709
+ ```
710
+
711
+ 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.
712
+
713
+ ```css
714
+ // /app/views/layouts/application.css
715
+ p {
716
+ @mixin bigText from url("/lib/mixins.css");
717
+ color: red;
718
+ }
719
+ ```
720
+
721
+ The above produce this output:
722
+
723
+ ```css
724
+ p {
725
+ font-size: 50px;
726
+ color: red;
727
+ }
728
+ ```
729
+
730
+ 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.
731
+
732
+ ```css
733
+ @define-mixin bigText {
734
+ font-size: 50px;
735
+ }
736
+
737
+ p {
738
+ @mixin bigText;
739
+ color: red;
740
+ }
741
+ ```
742
+
743
+ A mixin must be defined at the root of a file. A `@define-mixin` inside a rule or an at-rule block such as `@media`, or inside another mixin, does not define anything: it is passed through as written, with a warning.
744
+
745
+ One stylesheet may expand at most 100,000 mixins, inserting at most 4 MiB (4,194,304 bytes) of mixin definitions, with mixins nested at most 100 deep, so a chain of mixins that each include another more than once cannot grow without bound. Past any limit, each remaining `@mixin` is left as written, with one warning.
746
+
747
+ Each stylesheet reports at most 100 warnings, plus one saying how many more were not reported and, past a limit, the mixin limit warning. A precompile sends at most 100 errors and 100 warnings in all, then one more message for each saying how many more there were, so in a build that noisy the limit warning can be one of those counted.
748
+
749
+ CSS modules and Mixins works perfectly together. You can include a mixin in a CSS module.
750
+
751
+ ### CSS Caveats
752
+
753
+ 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).
754
+
755
+ ## Typescript
756
+
757
+ 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.
758
+
759
+ 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.
760
+
761
+ ### Typescript Caveats
762
+
763
+ 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).
764
+
765
+ ## JSX
766
+
767
+ JSX needs no imports. Proscenium uses esbuild's automatic JSX runtime, which imports what each JSX file needs for you, from React's runtime (`react/jsx-runtime`) by default.
768
+
769
+ To use another JSX library, such as Preact, set `jsxImportSource` in a `tsconfig.json` or `jsconfig.json`. It applies to every file in that directory and below:
770
+
771
+ ```json
772
+ {
773
+ "compilerOptions": {
774
+ "jsx": "react-jsx",
775
+ "jsxImportSource": "preact"
776
+ }
777
+ }
778
+ ```
779
+
780
+ Or set it for one file with a comment at the top:
781
+
782
+ ```jsx
783
+ /** @jsxImportSource preact */
784
+ export default () => <div>Hello</div>;
785
+ ```
786
+
787
+ ## JSON
788
+
789
+ 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:
790
+
791
+ ```javascript
792
+ import object from "./example.json";
793
+ console.log(object);
794
+ ```
795
+
796
+ 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:
797
+
798
+ ```javascript
799
+ import { version } from "./package.json";
800
+ console.log(version);
801
+ ```
802
+
803
+ ## rjs is back
804
+
805
+ Proscenium brings back RJS! Any path ending in .rjs will be served from your Rails app. This allows you to import server rendered javascript.
806
+
807
+ ## Testing your JavaScript
808
+
809
+ Your app's JavaScript can be tested with [Bun](https://bun.com), importing exactly what Proscenium
810
+ serves - root-absolute paths, extensionless imports, aliases, `@rubygems/*`, CSS modules, SVG
811
+ components, `proscenium/i18n`, `proscenium.env.*` and `.rjs` - with no dev server running and no
812
+ separate build step.
813
+
814
+ ```bash
815
+ rails generate proscenium:bun
816
+ ```
817
+
818
+ That writes `test/proscenium.preload.js` and adds it to your `bunfig.toml` (merging into an
819
+ existing one rather than replacing it). Then write a test that imports your app code:
820
+
821
+ ```jsx
822
+ // test/js/button.test.jsx
823
+ import { expect, test } from "bun:test";
824
+ import Button from "/app/components/button.jsx";
825
+ import styles from "/app/components/button.module.css";
826
+
827
+ test("the button has its scoped class name", () => {
828
+ expect(styles.button).toEqual(Button.className);
829
+ });
830
+ ```
831
+
832
+ ```bash
833
+ bun test
834
+ ```
835
+
836
+ ### What you test is what you ship
837
+
838
+ Every module you import is fetched through your application's own middleware stack, by a single
839
+ `rails runner` process the preload starts for the run and talks to over a Unix socket. Not rebuilt
840
+ with settings of the test harness's choosing - actually served, the same way a browser request is.
841
+ So your `config.proscenium` settings apply as-is: bundling, minification, code splitting, aliases,
842
+ externals and environment variables are whatever your app is configured to use. `.rjs` files are
843
+ rendered by your own routes.
844
+
845
+ The test file itself is the one exception, because no browser ever requests one: it is built
846
+ directly rather than served, with its output read as a string instead of written, code splitting
847
+ off, `bun:*`/`node:*` treated as external, and its source map inlined. Never minification, which is
848
+ the setting that would actually change what you are testing.
849
+
850
+ This matters more than it sounds. Minification decides the *shape* of a CSS module class name -
851
+ minified you get `button_a1b2c3d4`, unminified `button_a1b2c3d4_app-…` - and your views and your
852
+ stylesheets have to agree on which. So the harness does not get a vote: whatever your app is
853
+ configured to do is what runs. Proscenium's own suite fails if the two ever diverge.
854
+
855
+ Output is minified in production only, so a test failure names a real function on a real line
856
+ rather than a letter at column 80. Note that "production" means a literal `production` Rails
857
+ environment: any name Rails does not recognise - `staging`, `qa`, a per-PR environment - is
858
+ treated as test, and so is served and precompiled unminified.
859
+
860
+ ### Caveats
861
+
862
+ - **`mock.module` does not reach your own modules.** With bundling on - the Rails default - a
863
+ module's dependencies are inlined into it, so there is nothing left for a mock to substitute.
864
+ This catches an existing suite hard. Mock at a boundary the bundle cannot inline: `globalThis`
865
+ (`fetch`, a namespace a script installs), a property the code reads at call time, or a
866
+ test-environment `alias` pointing at a recording stub. Anything Proscenium leaves `external` -
867
+ `.rjs` included, since `*.rjs` is external by default - is resolved by Bun rather than inlined,
868
+ and a mock keyed on the specifier still will not match, because Bun resolves it to the file
869
+ Proscenium materialised. (`mock.module("/lib/api")` also cannot work at all: Bun only passes a
870
+ specifier to a plugin when it contains a `.` or a `:`.)
871
+ - **Put this preload last, and externalise the runner's own tooling.** The plugin's load hook has
872
+ no namespace filter, so once registered it claims every uncached JavaScript module - including
873
+ `@testing-library/react` and whatever else your harness needs from node_modules. Registering it
874
+ after those are loaded and cached is half the answer; adding them to `config.proscenium.external`
875
+ in the test environment is the other, or each served test file gets its own second copy.
876
+ - **An import map is invisible to Bun.** If bare specifiers reach the browser through
877
+ `<script type="importmap">`, mirror the same mapping in `compilerOptions.paths` in
878
+ `tsconfig.json`/`jsconfig.json`, which Bun does read. Otherwise Bun resolves them from
879
+ node_modules and you test a different copy than you ship - a CommonJS React, most likely, whose
880
+ named exports it cannot see.
881
+ - **No cache-busting query strings.** `await import("./thing.js?t=" + Date.now())` to force a
882
+ fresh module cannot work under a bundler, which fixes module identity at build time. Give the
883
+ module no state to reset instead, or reset it explicitly.
884
+ - **`sideEffects` in package.json is enforced.** A bare `import "./thing.js"` for its side effect
885
+ is dropped unless that path is listed there, exactly as in a production build.
886
+ - **Code splitting is off, and the harness turns it off for you.** Nothing on the JavaScript side
887
+ can resolve a `../_asset_chunks/<name>-$HASH$.js` specifier, so the daemon disables splitting in
888
+ its own process rather than asking you to disable it for your whole test environment - where it
889
+ would also cost your system tests the chunked output production emits. The cost is that a
890
+ dynamic `import()` runs here against an inlined module; the chunk fetch itself is a system
891
+ test's job.
892
+ - **Import statically.** Bun does not run a plugin's load hook for a dynamic `import()`, so
893
+ `await import("/lib/thing.js")` inside a test will not go through Proscenium.
894
+ - **`.rjs` actions need `skip_forgery_protection`.** Rails refuses a non-XHR GET that returns
895
+ JavaScript, in the browser as much as under test.
896
+ - **No DOM.** A CSS module still exports its class names, but the `<style>` element Proscenium
897
+ would append is skipped. Add [happy-dom](https://github.com/capricorn86/happy-dom) if you need
898
+ one.
899
+ - **`config.proscenium.bundle = false` needs ESM dependencies.** Unbundled, every module is loaded
900
+ on its own, and a JavaScript runtime cannot load a CommonJS package that way - React 18 included.
901
+ The same is true in the browser.
902
+ - **Stack traces name your functions, not your source lines.** Output is only minified in
903
+ production, so a failure points at a real function name and a real line of the built module.
904
+ It is not mapped back to the original file: a source map is embedded in everything the harness
905
+ builds, but Bun does not apply one to a module a plugin loaded (measured on Bun 1.3.13 and
906
+ 1.4.2). The map costs nothing to carry, so it stays for when Bun does.
907
+
908
+ - **Node, Deno and Vitest are not supported yet.** They can reuse the same daemon; see `TODOS.md`.
909
+
910
+ ## Resolution
911
+
912
+ 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`.
913
+
914
+ So a file at `/app/views/users/index.js` will be served from `https://localhost:3000/app/views/users/index.js`.
915
+
916
+ You can continue to access any file in the `/public` directory as you normally would. Proscenium will not process files in the `/public` directory.
917
+
918
+ 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.
919
+
920
+ ## Aliases
921
+
922
+ You can define import aliases via the `config.proscenium.aliases` config option. This allows you to create shorter or more meaningful import paths.
923
+
924
+ ```ruby
925
+ config.proscenium.aliases = {
926
+ "utils": "/lib/utils.js",
927
+ "components": "/app/components"
928
+ }
929
+ ```
930
+
931
+ You can then import using the alias:
932
+
933
+ ```js
934
+ import utils from "utils";
935
+ import Header from "components/header";
936
+ ```
937
+
938
+ ## Pre-compilation
939
+
940
+ Proscenium is designed to bundle and minify your frontend code in real time, on demand, with no build step or pre-compilation needed. However, if you want to pre-compile your assets for production deployment, you can do so using the `assets:precompile` Rake task.
941
+
942
+ ```bash
943
+ rails assets:precompile
944
+ ```
945
+
946
+ If any entry point fails to build, the task raises `Proscenium::Builder::CompileError` with esbuild's messages.
947
+
948
+ Be sure to specify a `Set` of paths which you want to pre-compile via the `config.proscenium.precompile` configuration option. Each path should be a glob pattern that matches the files which are your entry points. Don't include paths that are not entry points. For example:
949
+
950
+ ```ruby
951
+ Rails.configuration.proscenium.precompile = Set[
952
+ "./app/components/**/*.js",
953
+ "./app/components/**/*.jsx",
954
+ "./app/views/**/*.js",
955
+ "./app/views/**/*.css",
956
+ "./app/views/**/*.module.css"
957
+ ]
958
+ ```
959
+
960
+ This will bundle, code split, tree shake, and compile all your JS, TS, JSX, TSX and CSS files and place them in the `public/assets` directory, ready to be served in production.
961
+
962
+ ## Puma `preload_app!` and Cluster Mode
963
+
964
+ Proscenium's builder is backed by a Go shared library, and Go's runtime has a known, unfixed limitation ([golang/go#15538](https://github.com/golang/go/issues/15538)): if the Go runtime has already been initialized in a process before that process calls `fork()`, the forked child's Go runtime is left in a broken state (only the forking thread survives `fork()`; the Go scheduler and GC's other threads simply vanish) and any subsequent call into Go code in that child can hang or fail.
965
+
966
+ This matters if you run Puma in cluster mode with `preload_app!` (the app, including gems, is booted once in the master process, then workers are created via `fork()` with no `exec()` afterward). Proscenium itself never triggers this - simply requiring the gem does not initialize the Go runtime, and nothing in Proscenium's own boot sequence calls into it. The Go runtime only initializes lazily, the first time something actually calls a builder method (`build_to_string`, `resolve`, or `compile`).
967
+
968
+ **Do not call any `Proscenium::Builder` method (directly, or indirectly via the resolver/side-loading) from a Rails initializer or any other code that runs during application boot**, if you use `preload_app!` with `workers`. Doing so initializes the Go runtime in the master process before the fork, and every worker will inherit a broken one. There is no fix available from Go's side - this is a fundamental fork() limitation, not a bug Proscenium can work around. Asset builds and resolves triggered by actual HTTP requests (the normal case) are unaffected, since those always happen after the fork, independently in each worker.
969
+
970
+ ## Thanks
971
+
972
+ 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.
973
+
974
+ 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.
975
+
976
+ ## Development
977
+
978
+ 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:
979
+
980
+ ```bash
981
+ bundle exec rake compile:local
982
+ ```
983
+
984
+ ### Running tests
985
+
986
+ We have tests for both Ruby and Go. To run the Ruby tests:
987
+
988
+ ```bash
989
+ bin/test
990
+ ```
991
+
992
+ To run the Go tests:
993
+
994
+ ```bash
995
+ go test ./test ./internal/...
996
+ ```
997
+
998
+ To run the JavaScript tests, which need the Go library compiled first:
999
+
1000
+ ```bash
1001
+ cd fixtures/dummy && bun test test/js/
1002
+ ```
1003
+
1004
+ ### Running Go benchmarks
1005
+
1006
+ ```bash
1007
+ go test ./test -bench=. -run="^$" -count=10 -benchmem
1008
+ ```
1009
+
1010
+ ## Contributing
1011
+
1012
+ 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).
1013
+
1014
+ ## License
1015
+
1016
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
1017
+
1018
+ ## Code of Conduct
1019
+
1020
+ 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).