ruact 0.0.11 → 0.0.13
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +147 -48
- data/CONTRIBUTING.md +233 -0
- data/README.md +18 -8
- data/RELEASING.md +125 -139
- data/lib/generators/ruact/install/install_generator.rb +305 -126
- data/lib/generators/ruact/install/templates/AGENTS.md.tt +14 -13
- data/lib/generators/ruact/install/templates/initializer.rb.tt +29 -7
- data/lib/generators/ruact/install/templates/tsconfig.json.tt +3 -0
- data/lib/generators/ruact/layout/layout_generator.rb +52 -0
- data/lib/generators/ruact/scaffold/templates/controller.rb.tt +5 -3
- data/lib/ruact/configuration.rb +65 -13
- data/lib/ruact/controller/document_rendering.rb +72 -16
- data/lib/ruact/controller/page_rendering.rb +134 -0
- data/lib/ruact/controller/pages.rb +116 -0
- data/lib/ruact/controller.rb +78 -11
- data/lib/ruact/doctor.rb +233 -25
- data/lib/ruact/layout_source.rb +29 -7
- data/lib/ruact/navigation_boundary.rb +240 -0
- data/lib/ruact/packaging.rb +68 -0
- data/lib/ruact/railtie.rb +30 -0
- data/lib/ruact/server.rb +10 -1
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +158 -1
- data/lib/ruact/views/layouts/ruact.html.erb +32 -0
- data/lib/ruact.rb +29 -0
- data/vendor/javascript/vite-plugin-ruact/flight-client.test.mjs +321 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +11 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +1 -0
- data/vendor/javascript/vite-plugin-ruact/ruact-router.test.mjs +433 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +14 -3
- data/vendor/javascript/vite-plugin-ruact/runtime/ruact-router.js +170 -7
- metadata +12 -107
- data/.codecov.yml +0 -31
- data/.github/workflows/ci.yml +0 -284
- data/.github/workflows/server-functions-bench.yml +0 -54
- data/.rubocop.yml +0 -107
- data/.rubocop_todo.yml +0 -63
- data/Rakefile +0 -10
- data/bench/server_functions_dispatch_bench.rb +0 -276
- data/bench/server_functions_dispatch_bench.results.md +0 -150
- data/docs/internal/README.md +0 -9
- data/docs/internal/decisions/server-functions-api.md +0 -2236
- data/spec/benchmarks/baseline.json +0 -12
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +0 -109
- data/spec/fixtures/flight/README.md +0 -136
- data/spec/fixtures/flight/array.txt +0 -1
- data/spec/fixtures/flight/as_json_object.txt +0 -2
- data/spec/fixtures/flight/bigint.txt +0 -1
- data/spec/fixtures/flight/boolean_false.txt +0 -1
- data/spec/fixtures/flight/boolean_true.txt +0 -1
- data/spec/fixtures/flight/client_component_with_props.txt +0 -2
- data/spec/fixtures/flight/client_reference.txt +0 -2
- data/spec/fixtures/flight/hash.txt +0 -1
- data/spec/fixtures/flight/infinity.txt +0 -1
- data/spec/fixtures/flight/nan.txt +0 -1
- data/spec/fixtures/flight/negative_infinity.txt +0 -1
- data/spec/fixtures/flight/nil.txt +0 -1
- data/spec/fixtures/flight/number_float.txt +0 -1
- data/spec/fixtures/flight/number_integer.txt +0 -1
- data/spec/fixtures/flight/react_element_no_props.txt +0 -1
- data/spec/fixtures/flight/redirect_row.txt +0 -1
- data/spec/fixtures/flight/serializable_object.txt +0 -2
- data/spec/fixtures/flight/string_basic.txt +0 -1
- data/spec/fixtures/flight/string_dollar_escape.txt +0 -1
- data/spec/fixtures/flight/undefined.txt +0 -1
- data/spec/fixtures/readme/children-error.html.erb +0 -3
- data/spec/fixtures/readme/children-error.txt +0 -1
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +0 -3
- data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +0 -16
- data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +0 -24
- data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +0 -15
- data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +0 -17
- data/spec/readme_demo_message_spec.rb +0 -67
- data/spec/readme_spec.rb +0 -282
- data/spec/ruact/client_manifest_spec.rb +0 -270
- data/spec/ruact/component_contract_spec.rb +0 -119
- data/spec/ruact/configuration_spec.rb +0 -518
- data/spec/ruact/controller_request_spec.rb +0 -671
- data/spec/ruact/controller_spec.rb +0 -343
- data/spec/ruact/doctor_spec.rb +0 -769
- data/spec/ruact/erb_preprocessor_hook_spec.rb +0 -55
- data/spec/ruact/erb_preprocessor_spec.rb +0 -361
- data/spec/ruact/errors_spec.rb +0 -93
- data/spec/ruact/flight/renderer_spec.rb +0 -133
- data/spec/ruact/flight/serializer_spec.rb +0 -494
- data/spec/ruact/html_converter_spec.rb +0 -375
- data/spec/ruact/install_generator_spec.rb +0 -1549
- data/spec/ruact/layout_source_spec.rb +0 -108
- data/spec/ruact/manifest_resolver_spec.rb +0 -174
- data/spec/ruact/query_request_spec.rb +0 -706
- data/spec/ruact/query_spec.rb +0 -105
- data/spec/ruact/railtie_spec.rb +0 -155
- data/spec/ruact/render_context_spec.rb +0 -58
- data/spec/ruact/render_pipeline_concurrency_spec.rb +0 -78
- data/spec/ruact/render_pipeline_spec.rb +0 -928
- data/spec/ruact/scaffold_generator_spec.rb +0 -1849
- data/spec/ruact/serializable_spec.rb +0 -179
- data/spec/ruact/server_bucket_request_spec.rb +0 -785
- data/spec/ruact/server_function_name_spec.rb +0 -53
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +0 -63
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +0 -200
- data/spec/ruact/server_functions/codegen_spec.rb +0 -397
- data/spec/ruact/server_functions/error_payload_spec.rb +0 -222
- data/spec/ruact/server_functions/error_suggestion_spec.rb +0 -79
- data/spec/ruact/server_functions/introspection_spec.rb +0 -135
- data/spec/ruact/server_functions/name_bridge_spec.rb +0 -212
- data/spec/ruact/server_functions/query_context_spec.rb +0 -72
- data/spec/ruact/server_functions/query_source_spec.rb +0 -193
- data/spec/ruact/server_functions/railtie_integration_spec.rb +0 -215
- data/spec/ruact/server_functions/rake_spec.rb +0 -86
- data/spec/ruact/server_functions/route_source_spec.rb +0 -202
- data/spec/ruact/server_functions/snapshot_spec.rb +0 -96
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +0 -71
- data/spec/ruact/server_rescue_request_spec.rb +0 -416
- data/spec/ruact/server_spec.rb +0 -179
- data/spec/ruact/server_upload_request_spec.rb +0 -311
- data/spec/ruact/signed_references_spec.rb +0 -164
- data/spec/ruact/string_distance_spec.rb +0 -38
- data/spec/ruact/tasks_json_introspection_spec.rb +0 -141
- data/spec/ruact/testing/have_ruact_component_spec.rb +0 -170
- data/spec/ruact/testing/no_production_load_spec.rb +0 -41
- data/spec/ruact/validation_errors_spec.rb +0 -116
- data/spec/ruact/view_helper_spec.rb +0 -131
- data/spec/spec_helper.rb +0 -77
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +0 -21
- data/spec/support/flight_wire_parser_spec.rb +0 -93
- data/spec/support/matchers/flight_fixture_matcher.rb +0 -130
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +0 -250
- data/spec/support/rails_stub.rb +0 -115
|
@@ -11,11 +11,14 @@ module Ruact
|
|
|
11
11
|
#
|
|
12
12
|
# Performs the following actions:
|
|
13
13
|
# 1. Creates config/initializers/ruact.rb
|
|
14
|
-
# 2.
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
14
|
+
# 2. Changes no controller (island mode, Story 17.0g) — unless `--app`, which
|
|
15
|
+
# injects `include Ruact::Controller` into ApplicationController
|
|
16
|
+
# 3. Edits NO layout of the app. ruact pages render through the layout the
|
|
17
|
+
# gem ships (`config.layout = "ruact"`, Story 17.0b), which links the
|
|
18
|
+
# client-component CSS and the app stylesheets named in
|
|
19
|
+
# `config.layout_stylesheets`. An app that already chose its own layout
|
|
20
|
+
# (`config.layout = true` or another name) gets the missing lines PRINTED,
|
|
21
|
+
# never written. See Ruact::Configuration#layout.
|
|
19
22
|
# 4. Creates app/javascript/components/.keep
|
|
20
23
|
# 5. Creates vite.config.js (or shows manual instructions if one exists)
|
|
21
24
|
# 6. Creates package.json (react/react-dom/vite/@vitejs/plugin-react) so a
|
|
@@ -64,6 +67,15 @@ module Ruact
|
|
|
64
67
|
# the real shadcn CLI) and then PRINTS the two `npx shadcn` commands
|
|
65
68
|
# rather than running them — they hit the network and `shadcn init` is
|
|
66
69
|
# interactive, so automating them is neither safe nor possible.
|
|
70
|
+
# Story 17.0g (FR116) — the install changes no controller by default
|
|
71
|
+
# (island mode): a page becomes ruact when its controller includes
|
|
72
|
+
# `Ruact::Controller`. `--app` is the explicit way to the other mode.
|
|
73
|
+
class_option :app,
|
|
74
|
+
type: :boolean,
|
|
75
|
+
default: false,
|
|
76
|
+
desc: "Whole-app mode: include Ruact::Controller in ApplicationController, so EVERY action " \
|
|
77
|
+
"with an .html.erb template renders through ruact (and your own layout owns the document)"
|
|
78
|
+
|
|
67
79
|
class_option :shadcn,
|
|
68
80
|
type: :boolean,
|
|
69
81
|
default: false,
|
|
@@ -74,7 +86,7 @@ module Ruact
|
|
|
74
86
|
# offered a bad choice on the one path that matters most — migrating an
|
|
75
87
|
# existing app: overwrite and lose every setting the app had
|
|
76
88
|
# (`strict_serialization`, `manifest_path`, the SGID defaults…), or skip
|
|
77
|
-
# and end up half-migrated, with
|
|
89
|
+
# and end up half-migrated, with `config.layout`
|
|
78
90
|
# still off and nothing saying so except `ruact:doctor`.
|
|
79
91
|
def create_initializer
|
|
80
92
|
path = Pathname(destination_root).join("config/initializers/ruact.rb")
|
|
@@ -83,79 +95,61 @@ module Ruact
|
|
|
83
95
|
inject_layout_setting(path)
|
|
84
96
|
end
|
|
85
97
|
|
|
98
|
+
# Story 17.0g (FR116) — ONLY under `--app`. Including the concern in
|
|
99
|
+
# ApplicationController makes every action with a template a ruact page:
|
|
100
|
+
# installed into an existing app to try one screen, that converted all of
|
|
101
|
+
# them (a Turbo Frame came back as a React element). An app that already
|
|
102
|
+
# has it — installed before this — keeps it: removing a line of the app's
|
|
103
|
+
# is not the install's call, so it says what mode the app is in instead.
|
|
86
104
|
def inject_controller_concern
|
|
87
105
|
controller_file = "app/controllers/application_controller.rb"
|
|
88
|
-
|
|
106
|
+
path = Pathname(destination_root).join(controller_file)
|
|
107
|
+
unless path.exist?
|
|
108
|
+
if app?
|
|
109
|
+
say_status "notice", "no #{controller_file} — add `include Ruact::Controller` to the controller your " \
|
|
110
|
+
"pages inherit from", :yellow
|
|
111
|
+
end
|
|
112
|
+
return
|
|
113
|
+
end
|
|
89
114
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
say_status "skip", "Ruact::Controller already included in ApplicationController", :yellow
|
|
115
|
+
if path.read.match?(Ruact::CONTROLLER_INCLUDE)
|
|
116
|
+
advise_whole_app_install unless app?
|
|
93
117
|
return
|
|
94
118
|
end
|
|
119
|
+
return unless app?
|
|
95
120
|
|
|
96
121
|
inject_into_file controller_file,
|
|
97
122
|
"\n include Ruact::Controller\n",
|
|
98
123
|
after: /class ApplicationController.*\n/
|
|
99
124
|
end
|
|
100
125
|
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
# layout
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# A CALL, not a mention: `<%# TODO: add ruact_js_assets %>` used to read
|
|
116
|
-
# as "already present" here and skip the migration, leaving the app on
|
|
117
|
-
# ruact's CSS-less shell with the generator reporting success. Shared
|
|
118
|
-
# with the runtime so both agree on what "migrated" means.
|
|
119
|
-
# BOTH halves, not just the helper. A layout carrying `ruact_js_assets`
|
|
120
|
-
# with no `<div id="root"></div>` was skipped as "already present" — and
|
|
121
|
-
# since this generator also turns `config.layout` on, that app then
|
|
122
|
-
# raised at render time on a document React could not mount into.
|
|
123
|
-
if Ruact::LayoutSource.wired?(content) && Ruact::LayoutSource.root?(content)
|
|
124
|
-
say_status "skip", "ruact root + assets already present in layout", :yellow
|
|
126
|
+
# Story 17.0b — the install no longer writes into any layout of the app.
|
|
127
|
+
# An app on the gem's layout needs nothing. An app that already chose its
|
|
128
|
+
# OWN layout (`config.layout = true`, or a name that is not "ruact") has
|
|
129
|
+
# to call three helpers there; this reads that layout — never writes it —
|
|
130
|
+
# and prints each missing line with where it goes. Writing it was the
|
|
131
|
+
# previous design, and matching HTML+ERB with a regex took three review
|
|
132
|
+
# rounds without a correct version.
|
|
133
|
+
def advise_app_layout
|
|
134
|
+
layout = configured_app_layout
|
|
135
|
+
return unless layout
|
|
136
|
+
|
|
137
|
+
path = Pathname(destination_root).join("app/views/layouts/#{layout}.html.erb")
|
|
138
|
+
unless path.exist?
|
|
139
|
+
say_status "notice", "config.layout names #{path.basename}, which does not exist", :yellow
|
|
125
140
|
return
|
|
126
141
|
end
|
|
127
142
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
#
|
|
131
|
-
# The anchor matches the ROOT DIV itself rather than the marker-then-div
|
|
132
|
-
# pair, and tolerates the ways a real layout is written — single or
|
|
133
|
-
# double quotes, extra attributes, any attribute order, CRLF, and the
|
|
134
|
-
# marker on the same line. The earlier anchor required the exact emitted
|
|
135
|
-
# formatting, so a hand-edited layout silently matched nothing. The
|
|
136
|
-
# attribute boundary in `ROOT_ELEMENT` is what keeps `data-id="root"`
|
|
137
|
-
# from being mistaken for the mount point.
|
|
138
|
-
if Ruact::LayoutSource.root?(content)
|
|
139
|
-
return migrate_layout(layout_file,
|
|
140
|
-
"\n <%= ruact_js_assets %>",
|
|
141
|
-
after: Ruact::LayoutSource::ROOT_ELEMENT,
|
|
142
|
-
success: "added ruact_js_assets to the existing layout root")
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# The mirror case: the helper is there but the mount target is not, so
|
|
146
|
-
# the root goes in just BEFORE the call (React needs the node in the
|
|
147
|
-
# document, and keeping the pair adjacent matches what a fresh install
|
|
148
|
-
# writes). Injecting the whole block instead would duplicate the helper.
|
|
149
|
-
if Ruact::LayoutSource.wired?(content)
|
|
150
|
-
return migrate_layout(layout_file,
|
|
151
|
-
"<%# ruact: root %>\n <div id=\"root\"></div>\n ",
|
|
152
|
-
before: Ruact::LayoutSource::ASSETS_CALL,
|
|
153
|
-
success: "added the React root div next to the existing ruact_js_assets")
|
|
154
|
-
end
|
|
143
|
+
missing = missing_layout_lines(path.read)
|
|
144
|
+
return if missing.empty?
|
|
155
145
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
146
|
+
say_status "notice", "your layout renders ruact pages — add these lines to #{path.basename}:", :yellow
|
|
147
|
+
say ""
|
|
148
|
+
missing.each { |where, line| say " #{where}: #{line}" }
|
|
149
|
+
say ""
|
|
150
|
+
say " ruact does not edit your layout. `rails ruact:doctor` checks these lines,"
|
|
151
|
+
say " or set `config.layout = \"ruact\"` to use the layout ruact ships."
|
|
152
|
+
say ""
|
|
159
153
|
end
|
|
160
154
|
|
|
161
155
|
# `--shadcn` only. Two files, both of them things shadcn's CLI checks for
|
|
@@ -400,7 +394,9 @@ module Ruact
|
|
|
400
394
|
|
|
401
395
|
show_shadcn_next_steps if shadcn?
|
|
402
396
|
|
|
403
|
-
say "\nThen add <MyComponent /> to any ERB view
|
|
397
|
+
say "\nThen add <MyComponent /> to any ERB view."
|
|
398
|
+
say layout_summary
|
|
399
|
+
say ""
|
|
404
400
|
say "Note: re-run this generator after updating the ruact gem to refresh"
|
|
405
401
|
say "the bundled Vite plugin path in vite.config.js."
|
|
406
402
|
say ""
|
|
@@ -408,53 +404,185 @@ module Ruact
|
|
|
408
404
|
|
|
409
405
|
private
|
|
410
406
|
|
|
411
|
-
#
|
|
412
|
-
#
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if path.read == before
|
|
421
|
-
warn_layout_migration_failed
|
|
407
|
+
# What the post-install message says about the layout — true for the
|
|
408
|
+
# setting actually in effect, not just for a fresh install.
|
|
409
|
+
def layout_summary
|
|
410
|
+
[adoption_summary, document_summary].join("\n")
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def adoption_summary
|
|
414
|
+
if whole_app?
|
|
415
|
+
"Whole-app mode: every action with an .html.erb template renders through ruact."
|
|
422
416
|
else
|
|
423
|
-
|
|
417
|
+
"Island mode: nothing changed in your controllers. A page renders through ruact when its\n" \
|
|
418
|
+
"controller has `include Ruact::Controller` (narrow it with `ruact_pages only: %i[show]`),\n" \
|
|
419
|
+
"or run rails generate ruact:scaffold."
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def document_summary
|
|
424
|
+
own = configured_app_layout
|
|
425
|
+
return "ruact pages render through your #{own} layout (see any lines printed above)." if own
|
|
426
|
+
|
|
427
|
+
value = configured_layout_value
|
|
428
|
+
if value.nil? || value == "false"
|
|
429
|
+
return "ruact pages render through its built-in shell (config.layout is #{value || 'not set'})."
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
"ruact pages render through ruact's own layout — your layouts are untouched.\n" \
|
|
433
|
+
"To customize it: rails generate ruact:layout"
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Story 17.0b — what `layouts/ruact` should link, decided ONCE, here, and
|
|
437
|
+
# written into the initializer where the app can see and change it. Never
|
|
438
|
+
# inferred at render time. `:app` is what `rails new` 8.x links and means
|
|
439
|
+
# "every stylesheet" under Propshaft; under Sprockets it would be a file
|
|
440
|
+
# called app.css, which does not exist — an AssetNotFound on the first
|
|
441
|
+
# render — so a Sprockets app gets its conventional "application".
|
|
442
|
+
#
|
|
443
|
+
# Propshaft learned `:app` in 0.9.0 (checked against the released gems:
|
|
444
|
+
# 0.8.0's helper has no `when :app`, 0.9.0's does). Below that, or under
|
|
445
|
+
# Sprockets, the conventional "application" is what exists. With neither
|
|
446
|
+
# pipeline there is nothing to link: `:app` would become a 404 on
|
|
447
|
+
# /stylesheets/app.css.
|
|
448
|
+
PROPSHAFT_STYLESHEETS = "[:app]"
|
|
449
|
+
SPROCKETS_STYLESHEETS = '["application"]'
|
|
450
|
+
NO_PIPELINE_STYLESHEETS = "[]"
|
|
451
|
+
private_constant :PROPSHAFT_STYLESHEETS, :SPROCKETS_STYLESHEETS, :NO_PIPELINE_STYLESHEETS
|
|
452
|
+
|
|
453
|
+
def detected_layout_stylesheets
|
|
454
|
+
@detected_layout_stylesheets ||= begin
|
|
455
|
+
gems = locked_gems
|
|
456
|
+
propshaft = gems[/^ {4}propshaft \((\d+)\.(\d+)/] && [Regexp.last_match(1).to_i, Regexp.last_match(2).to_i]
|
|
457
|
+
if propshaft
|
|
458
|
+
(propshaft <=> [0, 9]) >= 0 ? PROPSHAFT_STYLESHEETS : SPROCKETS_STYLESHEETS
|
|
459
|
+
elsif gems.match?(/^ {4}sprockets-rails \(/)
|
|
460
|
+
SPROCKETS_STYLESHEETS
|
|
461
|
+
elsif gems.empty?
|
|
462
|
+
PROPSHAFT_STYLESHEETS # no lockfile to read: the default, as `rails new` would have
|
|
463
|
+
else
|
|
464
|
+
NO_PIPELINE_STYLESHEETS
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def locked_gems
|
|
470
|
+
%w[Gemfile.lock gems.locked].each do |name|
|
|
471
|
+
lock = Pathname(destination_root).join(name)
|
|
472
|
+
return lock.read if lock.exist?
|
|
473
|
+
end
|
|
474
|
+
""
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# The layout the app chose for ruact pages, when it is the app's OWN one:
|
|
478
|
+
# "application" for `config.layout = true`, the name for any other String
|
|
479
|
+
# but "ruact" (the gem's). nil when ruact's layout, the built-in shell, or
|
|
480
|
+
# no initializer is in play. Reads the initializer the app wrote, once, at
|
|
481
|
+
# install time — the same thing a reader of that file would conclude.
|
|
482
|
+
def configured_app_layout
|
|
483
|
+
value = configured_layout_value
|
|
484
|
+
return "application" if value == "true"
|
|
485
|
+
|
|
486
|
+
quoted = value && value[/\A["'](.+)["']\z/, 1]
|
|
487
|
+
name = quoted&.delete_prefix("layouts/")
|
|
488
|
+
return nil if name.nil? || name == Ruact::GEM_LAYOUT
|
|
489
|
+
|
|
490
|
+
name
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# The raw right-hand side of the LAST `…layout =` in the initializer — the
|
|
494
|
+
# one Ruby leaves in effect — or nil. Anything that is not a literal
|
|
495
|
+
# (`ENV.fetch(...)`, a ternary) comes back as-is and is treated as "not
|
|
496
|
+
# something to advise about", never guessed at.
|
|
497
|
+
def configured_layout_value
|
|
498
|
+
path = Pathname(destination_root).join("config/initializers/ruact.rb")
|
|
499
|
+
return nil unless path.exist?
|
|
500
|
+
|
|
501
|
+
layout_assignments(path.read).last
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# The right-hand side of every `<receiver>.layout =` outside a comment
|
|
505
|
+
# line, in order — the one-line `Ruact.configure { |c| c.layout = true }`
|
|
506
|
+
# the configuration docs show included. `layout_stylesheets =` never
|
|
507
|
+
# matches: `.layout` must be followed by `=`.
|
|
508
|
+
def layout_assignments(source)
|
|
509
|
+
source.each_line.reject { |line| line.lstrip.start_with?("#") }.flat_map do |line|
|
|
510
|
+
line.scan(/\b\w+\.layout\s*=(?!=)\s*([^\s#;}]+)/).flatten
|
|
424
511
|
end
|
|
425
512
|
end
|
|
426
513
|
|
|
514
|
+
# Each call the app's own layout is missing, with where it goes. Read
|
|
515
|
+
# through Ruact::LayoutSource, so a mention inside a comment is not a call
|
|
516
|
+
# — the same definition the doctor and the runtime use.
|
|
517
|
+
def missing_layout_lines(content)
|
|
518
|
+
missing = []
|
|
519
|
+
missing << ["inside <head>, above your stylesheets", "<%= ruact_head_assets %>"] unless
|
|
520
|
+
Ruact::LayoutSource.head_wired?(content)
|
|
521
|
+
missing << ["inside <body>", %(<div id="root"></div>)] unless Ruact::LayoutSource.root?(content)
|
|
522
|
+
missing << ["right after the root div", "<%= ruact_js_assets %>"] unless Ruact::LayoutSource.wired?(content)
|
|
523
|
+
missing
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# The block may name its variable anything (`|c|`, `|ruact|`): the setting
|
|
527
|
+
# is found whatever the receiver, and the snippet is written with the
|
|
528
|
+
# block's OWN variable — writing `config.` into a `|c|` block raised
|
|
529
|
+
# NameError at boot. One pattern both finds the block and anchors the
|
|
530
|
+
# injection, so the two cannot disagree (a trailing comment or CRLF used
|
|
531
|
+
# to pass the first check and miss the second, while the generator still
|
|
532
|
+
# reported success); and the file is compared before success is claimed.
|
|
427
533
|
def inject_layout_setting(path)
|
|
428
534
|
content = path.read
|
|
429
535
|
|
|
430
|
-
if content.
|
|
536
|
+
if layout_assignments(content).any?
|
|
431
537
|
say_status "skip", "config.layout already set in config/initializers/ruact.rb", :yellow
|
|
538
|
+
warn_whole_app_on_gem_layout if app? && configured_layout_value == %("#{Ruact::GEM_LAYOUT}")
|
|
432
539
|
return
|
|
433
540
|
end
|
|
434
541
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
542
|
+
blocks = content.scan(CONFIGURE_BLOCK)
|
|
543
|
+
# Exactly one multi-line block, or nothing: Thor's injection edits EVERY
|
|
544
|
+
# match, and a one-line block has no line of its own to write after.
|
|
545
|
+
return warn_initializer_not_injectable(content) unless blocks.length == 1
|
|
546
|
+
|
|
547
|
+
inject_into_file "config/initializers/ruact.rb", layout_setting_snippet(blocks.first.first),
|
|
548
|
+
after: CONFIGURE_BLOCK
|
|
549
|
+
return warn_initializer_not_injectable(content) if path.read == content && !options[:pretend]
|
|
439
550
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
551
|
+
if whole_app?
|
|
552
|
+
say_status "update", "set config.layout = true (your own layout renders every page)", :green
|
|
553
|
+
else
|
|
554
|
+
say_status "update", "set config.layout = \"ruact\" (ruact pages render through ruact's layout)", :green
|
|
555
|
+
end
|
|
444
556
|
end
|
|
445
557
|
|
|
446
|
-
# The compiled stylesheet still has to be REQUESTED
|
|
447
|
-
# layout
|
|
448
|
-
# every stylesheet on the load path — so `app/assets/builds/tailwind.css`
|
|
449
|
-
# is picked up with no further wiring (verified against a generated app:
|
|
450
|
-
# the rendered `<head>` carries `/assets/tailwind-<digest>.css`).
|
|
558
|
+
# The compiled Tailwind stylesheet still has to be REQUESTED, by whichever
|
|
559
|
+
# layout renders ruact pages.
|
|
451
560
|
#
|
|
452
|
-
#
|
|
453
|
-
#
|
|
454
|
-
#
|
|
455
|
-
#
|
|
561
|
+
# On ruact's own layout that is `config.layout_stylesheets`: `[:app]`
|
|
562
|
+
# (Propshaft) expands over every stylesheet on the load path, so
|
|
563
|
+
# `app/assets/builds/tailwind.css` is picked up with no further wiring. A
|
|
564
|
+
# Sprockets app gets `["application"]` instead, which does not name it.
|
|
565
|
+
#
|
|
566
|
+
# On the app's own layout (`config.layout = true` or another name) it is
|
|
567
|
+
# that layout's `stylesheet_link_tag`: `:app` picks the build up, a link
|
|
568
|
+
# BY NAME does not. Either way the failure is silent — Tailwind builds,
|
|
569
|
+
# the file is served, the page is unstyled — so warn rather than edit.
|
|
456
570
|
def warn_unless_layout_links_builds
|
|
457
|
-
|
|
571
|
+
layout = configured_app_layout
|
|
572
|
+
return warn_unless_app_layout_links_builds(layout) if layout
|
|
573
|
+
return unless detected_layout_stylesheets == SPROCKETS_STYLESHEETS
|
|
574
|
+
|
|
575
|
+
say_status "notice", "add the Tailwind build to the stylesheets ruact's layout links:", :yellow
|
|
576
|
+
say ""
|
|
577
|
+
say " config.layout_stylesheets = [\"application\", \"tailwind\"]"
|
|
578
|
+
say ""
|
|
579
|
+
say " in config/initializers/ruact.rb — `[\"application\"]` alone would leave"
|
|
580
|
+
say " app/assets/builds/tailwind.css built, served and never linked."
|
|
581
|
+
say ""
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def warn_unless_app_layout_links_builds(layout)
|
|
585
|
+
layout_path = Pathname(destination_root).join("app/views/layouts/#{layout}.html.erb")
|
|
458
586
|
return unless layout_path.exist?
|
|
459
587
|
|
|
460
588
|
content = layout_path.read
|
|
@@ -662,55 +790,106 @@ module Ruact
|
|
|
662
790
|
# Story 14.6 — a valid, lowercase npm "name" for the generated package.json,
|
|
663
791
|
# derived from the app directory. npm names must be lowercase and contain
|
|
664
792
|
# only URL-safe characters; anything else collapses to a hyphen.
|
|
665
|
-
# Kept
|
|
666
|
-
#
|
|
667
|
-
# the
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
793
|
+
# Kept in step with the `initializer.rb.tt` template's own `config.layout`
|
|
794
|
+
# block, so a migrated app and a fresh one end up reading the same thing.
|
|
795
|
+
# A method, not a constant, because the stylesheet list depends on the
|
|
796
|
+
# app's asset pipeline (see #detected_layout_stylesheets).
|
|
797
|
+
def layout_setting_snippet(variable = "config")
|
|
798
|
+
return app_layout_setting_snippet(variable) if whole_app?
|
|
799
|
+
|
|
800
|
+
<<~RUBY
|
|
801
|
+
# Render ruact pages through the layout ruact ships (layouts/ruact). It links
|
|
802
|
+
# the CSS your client components import, then your stylesheets below, and it
|
|
803
|
+
# edits none of your layouts. To change more of its <head> than the
|
|
804
|
+
# stylesheets, copy it into your app: `rails generate ruact:layout`.
|
|
805
|
+
#{variable}.layout = "ruact"
|
|
806
|
+
#{variable}.layout_stylesheets = #{detected_layout_stylesheets}
|
|
807
|
+
|
|
808
|
+
RUBY
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# Whole-app mode (`--app`): the document is the app's own layout, which
|
|
812
|
+
# then needs the helpers the install prints (see #advise_app_layout).
|
|
813
|
+
def app_layout_setting_snippet(variable)
|
|
814
|
+
<<~RUBY
|
|
815
|
+
# Whole-app mode: every page renders through your own layout, which needs
|
|
816
|
+
# <%= ruact_head_assets %> in <head> and <%= ruact_js_assets %> next to a
|
|
817
|
+
# <div id="root"></div> (rails ruact:doctor checks).
|
|
818
|
+
#{variable}.layout = true
|
|
819
|
+
|
|
820
|
+
RUBY
|
|
821
|
+
end
|
|
677
822
|
|
|
678
823
|
# The initializer exists but is not the shape we know how to edit (someone
|
|
679
824
|
# rewrote it, or wrapped the configure call). Never guess at it — say what
|
|
680
825
|
# to add, so the app cannot end up half-migrated in silence.
|
|
681
|
-
|
|
826
|
+
# The lines are written with the block's OWN variable when one can be read —
|
|
827
|
+
# `config.` pasted into a `{ |c| … }` block is the NameError at boot this
|
|
828
|
+
# generator used to cause.
|
|
829
|
+
def warn_initializer_not_injectable(content = "")
|
|
830
|
+
variable = content[/Ruact\.configure\s*(?:do|\{)\s*\|(\w+)\|/, 1] || "config"
|
|
682
831
|
say_status "skip", "could not find the Ruact.configure block to update", :red
|
|
683
832
|
say ""
|
|
684
|
-
say " Add
|
|
833
|
+
say " Add these lines inside `Ruact.configure` in config/initializers/ruact.rb:"
|
|
685
834
|
say ""
|
|
686
|
-
say "
|
|
835
|
+
say " #{variable}.layout = \"ruact\""
|
|
836
|
+
say " #{variable}.layout_stylesheets = #{detected_layout_stylesheets}"
|
|
687
837
|
say ""
|
|
688
|
-
say " Without
|
|
689
|
-
say "
|
|
838
|
+
say " Without them ruact keeps using its built-in shell, which carries none"
|
|
839
|
+
say " of your stylesheets — your app's CSS will not reach a ruact-rendered page."
|
|
690
840
|
say ""
|
|
691
841
|
end
|
|
692
842
|
|
|
693
|
-
|
|
694
|
-
|
|
843
|
+
# The configure block's opening line, whatever its variable is called: at
|
|
844
|
+
# the start of a line (so a commented-out example above it does not match),
|
|
845
|
+
# followed by nothing but whitespace or a comment (so a one-line block —
|
|
846
|
+
# body and `end` on the same line — does not match and get code written
|
|
847
|
+
# after its `end`, outside it), CRLF included.
|
|
848
|
+
CONFIGURE_BLOCK = /^[ \t]*Ruact\.configure\s+do\s*\|(\w+)\|[ \t]*(?:#[^\r\n]*)?\r?\n/
|
|
849
|
+
private_constant :CONFIGURE_BLOCK
|
|
850
|
+
|
|
851
|
+
def app?
|
|
852
|
+
options[:app]
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
def application_controller_includes_ruact?
|
|
856
|
+
path = Pathname(destination_root).join("app/controllers/application_controller.rb")
|
|
857
|
+
path.exist? && path.read.match?(Ruact::CONTROLLER_INCLUDE)
|
|
695
858
|
end
|
|
696
859
|
|
|
697
|
-
#
|
|
698
|
-
#
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
860
|
+
# Whole-app mode: asked for with `--app`, or already the app's state (an
|
|
861
|
+
# install from before 17.0g put the include on ApplicationController).
|
|
862
|
+
def whole_app?
|
|
863
|
+
app? || application_controller_includes_ruact?
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
# `--app` over an island install: every page now renders through the
|
|
867
|
+
# layout ruact ships, not the app's own — its <head>, fonts and JavaScript
|
|
868
|
+
# would be gone app-wide. Said, not changed: the setting is the app's.
|
|
869
|
+
def warn_whole_app_on_gem_layout
|
|
870
|
+
say_status "notice", "whole-app mode on ruact's own layout (config.layout = \"ruact\")", :yellow
|
|
703
871
|
say ""
|
|
704
|
-
say "
|
|
705
|
-
say "
|
|
872
|
+
say " Every page of the app will now render through the layout ruact ships, without"
|
|
873
|
+
say " your own layout's <head> and JavaScript. For whole-app mode you probably want"
|
|
874
|
+
say " config.layout = true in config/initializers/ruact.rb, with ruact_head_assets and"
|
|
875
|
+
say " ruact_js_assets in your layout (rails ruact:doctor checks)."
|
|
706
876
|
say ""
|
|
707
|
-
|
|
708
|
-
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
def advise_whole_app_install
|
|
880
|
+
say_status "notice", "this app is in whole-app mode (ApplicationController includes Ruact::Controller)", :yellow
|
|
709
881
|
say ""
|
|
710
|
-
say "
|
|
882
|
+
say " Every action with an .html.erb template renders through ruact. The install"
|
|
883
|
+
say " no longer does this by default; your app keeps it. To render only the pages"
|
|
884
|
+
say " you choose instead, remove `include Ruact::Controller` from ApplicationController"
|
|
885
|
+
say " and add it to the controllers whose pages should be ruact."
|
|
711
886
|
say ""
|
|
712
887
|
end
|
|
713
888
|
|
|
889
|
+
def shadcn?
|
|
890
|
+
options[:shadcn]
|
|
891
|
+
end
|
|
892
|
+
|
|
714
893
|
# The superset the scaffold generator narrows per resource. Loaded lazily
|
|
715
894
|
# (and only under `--shadcn`) so a plain install never pays for the
|
|
716
895
|
# scaffold generator's load, and so a failure to reach it degrades to the
|
|
@@ -15,7 +15,10 @@ truth" below) instead of guessing.
|
|
|
15
15
|
|
|
16
16
|
## Mental model
|
|
17
17
|
|
|
18
|
-
- A page is
|
|
18
|
+
- A page is an action rendering a `.html.erb` in a controller with `include Ruact::Controller`
|
|
19
|
+
(install adds it to none — `--app` puts it on ApplicationController; `ruact:scaffold`
|
|
20
|
+
adds it; `ruact_pages only:` narrows it). The page's renderer owns navigation:
|
|
21
|
+
going between a ruact page and a plain Rails/Turbo page is a full page load.
|
|
19
22
|
- Interactive components live in `app/javascript/components/` as `"use client"`
|
|
20
23
|
files, mounted from ERB with a PascalCase self-closing tag:
|
|
21
24
|
`<LikeButton postId={@post.id} />`.
|
|
@@ -66,10 +69,9 @@ object is instantiated.
|
|
|
66
69
|
|
|
67
70
|
## Ground truth — read the generated file
|
|
68
71
|
|
|
69
|
-
`app/javascript/.ruact/server-functions.ts`
|
|
70
|
-
table
|
|
71
|
-
|
|
72
|
-
Regenerate it after changing routes or queries:
|
|
72
|
+
`app/javascript/.ruact/server-functions.ts` (gitignored, regenerated from the
|
|
73
|
+
route table) is the authoritative list of every accessor and its typed params —
|
|
74
|
+
READ IT instead of simulating the name generation. Regenerate after routes/queries change:
|
|
73
75
|
|
|
74
76
|
bin/rails ruact:server_functions:generate
|
|
75
77
|
|
|
@@ -92,12 +94,12 @@ Regenerate it after changing routes or queries:
|
|
|
92
94
|
(a Flight stream for client-side navigation, an HTML page otherwise). You
|
|
93
95
|
cannot infer the response shape from the controller body alone — the
|
|
94
96
|
caller picks it.
|
|
95
|
-
4. **`ruact_errors` requires fall-through.** On
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
errors
|
|
97
|
+
4. **`ruact_errors` requires fall-through.** On a function call's failed-save
|
|
98
|
+
branch, call `ruact_errors(@post)` and let the action END — the implicit
|
|
99
|
+
render injects `errors: { attribute: [messages] }` into the JSON (an explicit
|
|
100
|
+
`render` opts out). On a page form, `ruact_errors(@post)` then
|
|
101
|
+
`render :new, status: :unprocessable_entity` re-renders the page through ruact
|
|
102
|
+
(view: `errors={ruact_errors}`); `redirect_to` instead carries them via flash.
|
|
101
103
|
5. **Accessor names are derived, not declared.** `posts#create` → `createPost`,
|
|
102
104
|
`posts#publish_all` → `publishAllPosts`; query methods camelCase the same
|
|
103
105
|
way (`search_users` → `searchUsers`). Collisions fail loudly at boot;
|
|
@@ -146,8 +148,7 @@ duration, or a deliberate `expires_in: nil` for a non-expiring token.
|
|
|
146
148
|
`schema_version` field (currently `0`), do not treat it as a stable contract.
|
|
147
149
|
- `bin/rails ruact:server_functions:generate` — regenerates the TS module;
|
|
148
150
|
exits 1 on a naming collision or an invalid name.
|
|
149
|
-
- `bin/dev` — boots Rails AND Vite (both
|
|
150
|
-
components and writes the client manifest).
|
|
151
|
+
- `bin/dev` — boots Rails AND Vite (both required: Vite serves the client components).
|
|
151
152
|
- If your app has TypeScript tooling configured, `npx tsc --noEmit`
|
|
152
153
|
type-checks call sites against the generated accessor types (a fresh
|
|
153
154
|
install does not ship a tsconfig).
|
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
Ruact.configure do |config|
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
4
|
+
<% if whole_app? -%>
|
|
5
|
+
# Whole-app mode (`rails generate ruact:install --app`, or an app installed
|
|
6
|
+
# before island mode was the default): ApplicationController
|
|
7
|
+
# includes Ruact::Controller, so every action with an .html.erb template
|
|
8
|
+
# renders through ruact, into your own layout. That layout needs
|
|
9
|
+
# `<%%= ruact_head_assets %>` in <head> and `<%%= ruact_js_assets %>` next to a
|
|
10
|
+
# `<div id="root"></div>` (`rails ruact:doctor` checks).
|
|
11
11
|
config.layout = true
|
|
12
|
+
<% else -%>
|
|
13
|
+
# Island mode: nothing in your controllers changed. A page renders through
|
|
14
|
+
# ruact when its controller has `include Ruact::Controller` — narrow it to
|
|
15
|
+
# some actions with `ruact_pages only: %i[show]`.
|
|
16
|
+
#
|
|
17
|
+
# Ruact pages render through the layout ruact ships (layouts/ruact). It links
|
|
18
|
+
# the CSS your client components import, then the stylesheets below — yours
|
|
19
|
+
# load last, so they win ties — and it edits none of your layouts. What it
|
|
20
|
+
# does NOT bring is the rest of your own layout's <head> or your app's
|
|
21
|
+
# JavaScript; to own that document, copy it into your app with
|
|
22
|
+
# `rails generate ruact:layout`.
|
|
23
|
+
#
|
|
24
|
+
# Set to true to render through your app's own layout instead (it then needs
|
|
25
|
+
# `<%%= ruact_head_assets %>` in <head> and `<%%= ruact_js_assets %>` next to a
|
|
26
|
+
# `<div id="root"></div>`; `rails ruact:doctor` checks), or false for ruact's
|
|
27
|
+
# minimal built-in shell, which carries none of your stylesheets.
|
|
28
|
+
config.layout = "ruact"
|
|
29
|
+
|
|
30
|
+
# The app stylesheets ruact's layout links, as you would pass them to
|
|
31
|
+
# stylesheet_link_tag. [:app] under Propshaft is every stylesheet you have.
|
|
32
|
+
config.layout_stylesheets = <%= detected_layout_stylesheets %>
|
|
33
|
+
<% end -%>
|
|
12
34
|
|
|
13
35
|
# Path to the react-client-manifest.json generated by the Vite plugin.
|
|
14
36
|
# Defaults to Rails.root.join("public/react-client-manifest.json").
|