proscenium 0.25.3 → 0.26.0

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +170 -23
  3. data/exe/proscenium +19 -0
  4. data/exe/psm +5 -0
  5. data/lib/proscenium/app_package.rb +90 -0
  6. data/lib/proscenium/builder.rb +221 -20
  7. data/lib/proscenium/bundled_gems.rb +103 -0
  8. data/lib/proscenium/cli/brackets.rb +66 -0
  9. data/lib/proscenium/cli/bun_linker.rb +79 -0
  10. data/lib/proscenium/cli/capabilities.json +29 -0
  11. data/lib/proscenium/cli/collisions.rb +94 -0
  12. data/lib/proscenium/cli/contexts.rb +133 -0
  13. data/lib/proscenium/cli/diff.rb +80 -0
  14. data/lib/proscenium/cli/error.rb +291 -0
  15. data/lib/proscenium/cli/gem_archive.rb +44 -0
  16. data/lib/proscenium/cli/gem_check.rb +150 -0
  17. data/lib/proscenium/cli/gitignore.rb +63 -0
  18. data/lib/proscenium/cli/inspect.rb +257 -0
  19. data/lib/proscenium/cli/install.rb +378 -0
  20. data/lib/proscenium/cli/manager.rb +323 -0
  21. data/lib/proscenium/cli/platform_variants.rb +82 -0
  22. data/lib/proscenium/cli/pnpm_approvals.rb +109 -0
  23. data/lib/proscenium/cli/pnpm_workspace.rb +149 -0
  24. data/lib/proscenium/cli/project_lock.rb +106 -0
  25. data/lib/proscenium/cli/registration.rb +140 -0
  26. data/lib/proscenium/cli/reporter.rb +103 -0
  27. data/lib/proscenium/cli/rules.rb +169 -0
  28. data/lib/proscenium/cli/runner.rb +142 -0
  29. data/lib/proscenium/cli/verify.rb +107 -0
  30. data/lib/proscenium/cli.rb +122 -0
  31. data/lib/proscenium/context_map.rb +181 -0
  32. data/lib/proscenium/css_module/transformer.rb +18 -3
  33. data/lib/proscenium/css_module.rb +5 -6
  34. data/lib/proscenium/dependency_context.rb +74 -0
  35. data/lib/proscenium/error.rb +10 -0
  36. data/lib/proscenium/helper.rb +38 -4
  37. data/lib/proscenium/importer.rb +53 -42
  38. data/lib/proscenium/manifest.rb +18 -23
  39. data/lib/proscenium/mapping_generation.rb +64 -0
  40. data/lib/proscenium/middleware/base.rb +5 -10
  41. data/lib/proscenium/middleware/chunks.rb +5 -0
  42. data/lib/proscenium/monkey.rb +51 -69
  43. data/lib/proscenium/railtie.rb +16 -0
  44. data/lib/proscenium/railties/assets.rake +2 -1
  45. data/lib/proscenium/resolver.rb +43 -11
  46. data/lib/proscenium/runtime/bootstrap.js +26 -6
  47. data/lib/proscenium/runtime/server.rb +57 -36
  48. data/lib/proscenium/side_load.rb +75 -18
  49. data/lib/proscenium/stale_contexts.rb +118 -0
  50. data/lib/proscenium/templates/rescues/build_error.html.erb +9 -7
  51. data/lib/proscenium/utils.rb +55 -0
  52. data/lib/proscenium/version.rb +1 -1
  53. data/lib/proscenium/yaml_tree.rb +48 -0
  54. data/lib/proscenium.rb +7 -4
  55. metadata +42 -13
  56. data/lib/proscenium/ext/proscenium +0 -0
  57. data/lib/proscenium/ext/proscenium.h +0 -113
  58. data/lib/proscenium/react-manager/index.jsx +0 -102
  59. data/lib/proscenium/react-manager/react.js +0 -2
  60. data/lib/proscenium/react_componentable.rb +0 -94
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9b8a9a1d58d4345c62b41f8903e0515cc93f205b40ea91c8316702b8cf682b7
4
- data.tar.gz: '0947a3b0b28e96d348a90a086b41266830613ff3f8786bcaf75ddabaa203705a'
3
+ metadata.gz: 2c5911cfff78cd611fec6681fe3bbda4890a72da2c7d25b226facbfb0c77a7a3
4
+ data.tar.gz: afdf60be93b068941e2f0496bba05a2be1af3389d9a98f4e010b1027da86b72b
5
5
  SHA512:
6
- metadata.gz: 8ed1775133d82c9be954930a924f075ca3b3ebf1fe0f427444b02f4603eb3ad74ab9856661839f431d0311a762a53c8d22a7d7b8139ce8347e3281acaa8757f5
7
- data.tar.gz: 5fd429b60fc41e5eb18f5147a965c4ca221f46e2d59e5091ba4e06325f539a3be79117e1bfde943db36885f895461d99dd4c8c9416bff1e2fded18724d5a0651
6
+ metadata.gz: 8da40b3b94f126ad0565e2bed0fefbe18d268fd43a4b185c0328c18163dec3cb6a385f6a7ae235482f6c6e30deb7d189b980cfa1a96f9d10b5808f4bb3a2765f
7
+ data.tar.gz: 5852c1d9b9d307b5a0072ab1669413822aafb33b211f33720787f2b554426860de9d751a4423e6e25dece3634e40ccb62e2bd4b4725d341b1cfc4b5430584917
data/README.md CHANGED
@@ -25,10 +25,14 @@
25
25
 
26
26
  - [Getting Started](#getting-started)
27
27
  - [Installation](#installation)
28
+ - [Supported platforms](#supported-platforms)
28
29
  - [Client-Side Code Anywhere](#client-side-code-anywhere)
29
30
  - [Side Loading](#side-loading)
30
- - [Importing](#importing-assets)
31
+ - [Bundling](#bundling)
32
+ - [Import from NPM (`node_modules`)](#import-from-npm-node_modules)
33
+ - [RubyGem NPM dependencies](#rubygem-npm-dependencies)
31
34
  - [Local Imports](#local-imports)
35
+ - [Unbundling](#unbundling)
32
36
  - [Source Maps](#source-maps)
33
37
  - [SVG](#svg)
34
38
  - [Environment Variables](#environment-variables)
@@ -51,7 +55,7 @@
51
55
  - [Testing your JavaScript](#testing-your-javascript)
52
56
  - [Resolution](#resolution)
53
57
  - [Aliases](#aliases)
54
- - [Pre-compilation](#precompilation)
58
+ - [Pre-compilation](#pre-compilation)
55
59
  - [Puma `preload_app!` and Cluster Mode](#puma-preload_app-and-cluster-mode)
56
60
  - [Thanks](#thanks)
57
61
  - [Development](#development)
@@ -63,7 +67,8 @@ Getting started obviously depends on whether you are adding Proscenium to an exi
63
67
  - [Getting Started with a new Rails app](docs/guides/new_rails_app.md)
64
68
  - Getting Started with an existing Rails app
65
69
  - [Migrate from Sprockets](docs/guides/migrate_from_sprockets.md)
66
- - [Render a React component with Proscenium](docs/guides/basic_react.md)
70
+ - [RubyGem NPM dependencies](docs/guides/rubygem_npm_dependencies.md)
71
+ - [Shipping NPM dependencies in a gem](docs/guides/gem_author.md)
67
72
 
68
73
  ## Installation
69
74
 
@@ -73,10 +78,69 @@ Add this line to your Rails application's Gemfile, and you're good to go:
73
78
  gem 'proscenium'
74
79
  ```
75
80
 
76
- Please note that Proscenium is designed solely for use with Rails.
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.
77
83
 
78
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.
79
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
+
80
144
  ## Client-Side Code Anywhere
81
145
 
82
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!
@@ -182,7 +246,7 @@ Now, in your layout and view, replace the `javascript_include_tag` and `styleshe
182
246
  </html>
183
247
  ```
184
248
 
185
- 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 your placed the `include_assets` helper.
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.
186
250
 
187
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.
188
252
 
@@ -190,6 +254,31 @@ Side loading is enabled by default, but you can disable it by setting `config.pr
190
254
 
191
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.
192
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
+
193
282
  ## Bundling
194
283
 
195
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.
@@ -218,7 +307,7 @@ import(`./locale-${foo}.json`);
218
307
  import(`pkg/${foo}`);
219
308
  ```
220
309
 
221
- 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.
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.
222
311
 
223
312
  ### Import from NPM (`node_modules`)
224
313
 
@@ -236,6 +325,32 @@ npm install react
236
325
  import React from "react";
237
326
  ```
238
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
+
239
354
  ### Local Imports
240
355
 
241
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):
@@ -296,6 +411,10 @@ Your browsers dev tools should pick this up and automatically load the source ma
296
411
 
297
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.
298
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
+
299
418
  ## Environment Variables
300
419
 
301
420
  You can define and access any environment variable from your JavaScript and Typescript under the `proscenium.env` namespace.
@@ -303,10 +422,13 @@ You can define and access any environment variable from your JavaScript and Type
303
422
  For performance and security reasons you must declare the environment variable names that you wish to expose in your `config/application.rb` file.
304
423
 
305
424
  ```ruby
306
- config.proscenium.env_vars = Set['API_KEY', 'SOME_SECRET_VARIABLE']
307
- config.proscenium.env_vars << 'ANOTHER_API_KEY'
425
+ config.proscenium.env_vars = Set['APP_VERSION', 'PUBLIC_API_URL']
426
+ config.proscenium.env_vars << 'SENTRY_DSN'
308
427
  ```
309
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
+
310
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.
311
433
 
312
434
  These declared environment variables will be replaced with constant expressions, allowing you to use this like this:
@@ -335,7 +457,7 @@ proscenium.env.RAILS_ENV === "development" && doSomethingDangerous();
335
457
  start();
336
458
  ```
337
459
 
338
- In development the above code will be transformed into the following code, discarding the definition, and call to`doSomethingDangerous()`.
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()`.
339
461
 
340
462
  ```js
341
463
  function start() {
@@ -371,6 +493,10 @@ import translations from "proscenium/i18n";
371
493
 
372
494
  If you have multiple locale files, they will be merged together. into one json object.
373
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
+
374
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.
375
501
 
376
502
  ## Javascript
@@ -520,13 +646,13 @@ With [side-loading](#side-loading) setup, you can use the `css_module` helper as
520
646
  ```erb
521
647
  <div>
522
648
  <h1 class="<%= css_module :hello_title %>">Hello World</h1>
523
- <p class="<%= css_module :body, paragraph: %>">
649
+ <p class="<%= css_module :body, :paragraph %>">
524
650
  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
525
651
  </p>
526
652
  </div>
527
653
  ```
528
654
 
529
- `css_module` accepts multiple class names, and will return a space-separated string of transformed CSS module names.
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.
530
656
 
531
657
  ```ruby
532
658
  css_module :my_module_name
@@ -614,6 +740,12 @@ p {
614
740
  }
615
741
  ```
616
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
+
617
749
  CSS modules and Mixins works perfectly together. You can include a mixin in a CSS module.
618
750
 
619
751
  ### CSS Caveats
@@ -632,18 +764,25 @@ There are a few important caveats as far as Typescript is concerned. These are [
632
764
 
633
765
  ## JSX
634
766
 
635
- 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:
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.
636
768
 
637
- ```javascript
638
- import * as React from "react";
639
- render(<div />);
640
- ```
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:
641
770
 
642
- 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.
771
+ ```json
772
+ {
773
+ "compilerOptions": {
774
+ "jsx": "react-jsx",
775
+ "jsxImportSource": "preact"
776
+ }
777
+ }
778
+ ```
643
779
 
644
- 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.
780
+ Or set it for one file with a comment at the top:
645
781
 
646
- 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.
782
+ ```jsx
783
+ /** @jsxImportSource preact */
784
+ export default () => <div>Hello</div>;
785
+ ```
647
786
 
648
787
  ## JSON
649
788
 
@@ -763,8 +902,8 @@ treated as test, and so is served and precompiled unminified.
763
902
  - **Stack traces name your functions, not your source lines.** Output is only minified in
764
903
  production, so a failure points at a real function name and a real line of the built module.
765
904
  It is not mapped back to the original file: a source map is embedded in everything the harness
766
- builds, but Bun does not apply one to a module a plugin loaded (measured on Bun 1.3.13). The map
767
- costs nothing to carry, so it stays for when Bun does.
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.
768
907
 
769
908
  - **Node, Deno and Vitest are not supported yet.** They can reuse the same daemon; see `TODOS.md`.
770
909
 
@@ -804,6 +943,8 @@ Proscenium is designed to bundle and minify your frontend code in real time, on
804
943
  rails assets:precompile
805
944
  ```
806
945
 
946
+ If any entry point fails to build, the task raises `Proscenium::Builder::CompileError` with esbuild's messages.
947
+
807
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:
808
949
 
809
950
  ```ruby
@@ -851,13 +992,19 @@ bin/test
851
992
  To run the Go tests:
852
993
 
853
994
  ```bash
854
- go test ./test
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/
855
1002
  ```
856
1003
 
857
1004
  ### Running Go benchmarks
858
1005
 
859
1006
  ```bash
860
- go test ./internal/builder -bench=. -run="^$" -count=10 -benchmem
1007
+ go test ./test -bench=. -run="^$" -count=10 -benchmem
861
1008
  ```
862
1009
 
863
1010
  ## Contributing
data/exe/proscenium ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Run as `bundle exec proscenium`. Loads the CLI alone: never `proscenium` itself, Rails, FFI or
5
+ # the engine library (see lib/proscenium/cli.rb).
6
+ # When the CLI started loading, for the timings install reports (C40).
7
+ PROSCENIUM_STARTED = Process.clock_gettime(Process::CLOCK_MONOTONIC)
8
+
9
+ # Every file the CLI reads (package.json, contexts, pnpm-workspace.yaml) is UTF-8, whatever the
10
+ # locale: under LANG=C, as minimal Docker and CI images run, Ruby would read them as US-ASCII.
11
+ # Ruby warns of the setting, which is meant for this process alone.
12
+ verbose = $VERBOSE
13
+ $VERBOSE = nil
14
+ Encoding.default_external = Encoding::UTF_8
15
+ $VERBOSE = verbose
16
+
17
+ require 'proscenium/cli'
18
+
19
+ exit Proscenium::CLI.start(ARGV)
data/exe/psm ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # `bundle exec psm`: a shorter name for `bundle exec proscenium`, which it runs as is.
5
+ load File.expand_path('proscenium', __dir__)
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Proscenium
6
+ # What the app's own package.json says that the context map needs (#154). Like ContextMap, which
7
+ # it reads JSON through, it loads nothing the `proscenium` CLI may not.
8
+ module AppPackage
9
+ LOCAL_SPECS = %w[link: file: workspace: portal:].freeze
10
+
11
+ module_function
12
+
13
+ # The names of the app's own `link:`, `file:` and workspace dependencies, peers included, as
14
+ # pnpm installs those too, and in a Bun app any workspace member it depends on by version,
15
+ # which Bun links. They keep their link paths once dependency contexts make other packages
16
+ # real-path.
17
+ def local_packages(root)
18
+ package = read(root) or return []
19
+
20
+ members = bun?(root, package) ? workspace_members(root, package['workspaces']) : []
21
+ %w[dependencies devDependencies optionalDependencies peerDependencies].flat_map do |field|
22
+ deps = package[field]
23
+ next [] unless deps.is_a?(Hash)
24
+
25
+ deps.select { |name, spec| spec.to_s.start_with?(*LOCAL_SPECS) || members.include?(name) }
26
+ .keys
27
+ end.uniq.sort
28
+ end
29
+
30
+ # Whether Bun installs the app: packageManager names it, or else its lockfile is there.
31
+ def bun?(root, package)
32
+ named = package['packageManager']
33
+ return named.split('@').first == 'bun' if named.is_a?(String)
34
+
35
+ %w[bun.lock bun.lockb].any? { File.exist?(File.join(root, it)) }
36
+ end
37
+
38
+ # The names of the packages `workspaces` (an array, or an object's `packages`) takes in, less
39
+ # those a `!` pattern leaves out, read as Bun's glob does: braces and `**` included.
40
+ def workspace_members(root, workspaces)
41
+ patterns = Array(workspaces.is_a?(Hash) ? workspaces['packages'] : workspaces).grep(String)
42
+ excluded, included = patterns.map { it.delete_prefix('./').chomp('/') }
43
+ .partition { it.start_with?('!') }
44
+ dirs = included.flat_map { Dir.glob(it, File::FNM_EXTGLOB, base: root) }.uniq
45
+ dirs.reject { |dir| excluded.any? { File.fnmatch?(it[1..], dir, File::FNM_EXTGLOB) } }
46
+ .filter_map { read(File.join(root, it))&.dig('name') }
47
+ end
48
+
49
+ # Whether package.json's text, for a file JSON.parse refuses, registers the contexts in its
50
+ # `workspaces`, decoding JSON's `\/` and `\uXXXX`
51
+ # escapes in the key and the pattern, as Bun does.
52
+ def workspaces?(text) = unescape(text).match?(WORKSPACES)
53
+
54
+ # JSON text with its `\uXXXX` and `\/` escapes decoded, as Bun reads them, for matching a file
55
+ # JSON.parse refuses.
56
+ # A surrogate pair is one character; a lone half, which JSON allows, becomes U+FFFD rather
57
+ # than raising.
58
+ def unescape(text)
59
+ text.gsub(ESCAPE) do
60
+ high, low, single = ::Regexp.last_match.captures
61
+ if high then (0x10000 + ((high.hex - 0xD800) << 10) + (low.hex - 0xDC00)).chr(Encoding::UTF_8)
62
+ elsif single then SURROGATES.cover?(single.hex) ? "\uFFFD" : single.hex.chr(Encoding::UTF_8)
63
+ else '/'
64
+ end
65
+ end
66
+ end
67
+
68
+ ESCAPE = %r{\\u([dD][89abAB]\h{2})\\u([dD][c-fC-F]\h{2})|\\u(\h{4})|\\/}
69
+ SURROGATES = (0xD800..0xDFFF)
70
+
71
+ # The pattern inside package.json's `workspaces`, an array or an object's `packages`.
72
+ WORKSPACES = /"workspaces"\s*:\s*(?:\{[^}]*"packages"\s*:\s*)?\[[^\]]*
73
+ "\.proscenium\/packages\/\*"/x
74
+
75
+ # The manager `packageManager` names, or nil.
76
+ def package_manager(root)
77
+ field = read(root)&.dig('packageManager')
78
+ field.split('@').first if field.is_a?(String)
79
+ end
80
+
81
+ # The app's package.json as an object, or nil when it is missing or not one.
82
+ def read(root)
83
+ path = File.join(root, 'package.json')
84
+ package = File.exist?(path) && ContextMap.read_json(path)
85
+ package if package.is_a?(Hash)
86
+ rescue JSON::ParserError
87
+ nil
88
+ end
89
+ end
90
+ end