ruact 0.0.8 → 0.0.9

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -1
  3. data/lib/generators/ruact/install/install_generator.rb +264 -5
  4. data/lib/generators/ruact/install/templates/Procfile.dev.tt +3 -0
  5. data/lib/generators/ruact/install/templates/globals.css.tt +20 -0
  6. data/lib/generators/ruact/install/templates/initializer.rb.tt +9 -0
  7. data/lib/generators/ruact/install/templates/package.json.tt +7 -1
  8. data/lib/generators/ruact/install/templates/tsconfig.json.tt +18 -0
  9. data/lib/generators/ruact/scaffold/scaffold_shadcn_preflight.rb +10 -0
  10. data/lib/ruact/configuration.rb +73 -0
  11. data/lib/ruact/controller/document_rendering.rb +210 -0
  12. data/lib/ruact/controller.rb +5 -46
  13. data/lib/ruact/doctor.rb +37 -5
  14. data/lib/ruact/layout_source.rb +59 -0
  15. data/lib/ruact/version.rb +1 -1
  16. data/lib/ruact/view_helper.rb +10 -1
  17. data/lib/ruact.rb +1 -0
  18. data/spec/fixtures/story_7_9_views/controller_request_spec_support/exploding_layout_demo/show.html.erb +3 -0
  19. data/spec/fixtures/story_7_9_views/controller_request_spec_support/ghost_layout_demo/show.html.erb +3 -0
  20. data/spec/fixtures/story_7_9_views/controller_request_spec_support/layout_demo/show.html.erb +3 -0
  21. data/spec/fixtures/story_7_9_views/controller_request_spec_support/rootless_layout_demo/show.html.erb +3 -0
  22. data/spec/fixtures/story_7_9_views/controller_request_spec_support/unwired_layout_demo/show.html.erb +3 -0
  23. data/spec/fixtures/story_7_9_views/layouts/bare_host.html.erb +16 -0
  24. data/spec/fixtures/story_7_9_views/layouts/exploding_host.html.erb +24 -0
  25. data/spec/fixtures/story_7_9_views/layouts/rootless_host.html.erb +15 -0
  26. data/spec/fixtures/story_7_9_views/layouts/ruact_host.html.erb +17 -0
  27. data/spec/ruact/controller_request_spec.rb +203 -0
  28. data/spec/ruact/doctor_spec.rb +81 -6
  29. data/spec/ruact/install_generator_spec.rb +442 -70
  30. data/spec/ruact/layout_source_spec.rb +108 -0
  31. data/spec/ruact/scaffold_generator_spec.rb +14 -0
  32. metadata +18 -4
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruact
4
+ module Controller
5
+ # How a ruact response becomes a full HTML DOCUMENT — the wrapper a browser
6
+ # gets on a normal navigation, as opposed to the raw `text/x-component`
7
+ # Flight body an in-app navigation gets.
8
+ #
9
+ # Split out of `Ruact::Controller` because it answers one self-contained
10
+ # question ("who owns the `<head>`?") whose answer is load-bearing: the host
11
+ # app's layout does, because `stylesheet_link_tag`, favicons, fonts,
12
+ # analytics and every `<head>`-writing gem live there. `#ruact_html_shell`
13
+ # is the fallback for an app whose layout has not been migrated — it is
14
+ # deliberately minimal and has NO stylesheet slot.
15
+ module DocumentRendering
16
+ extend ActiveSupport::Concern
17
+
18
+ # Proof that a host layout called `ruact_js_assets`: the helper always
19
+ # emits the `__FLIGHT_DATA` bootstrap script when a payload is present,
20
+ # and `render_ruact_document` always supplies one. Matching on the payload
21
+ # script (rather than on the entry `<script src>`) keeps the check true in
22
+ # BOTH dev and production, whose entry tags differ.
23
+ RUACT_ASSETS_MARKER = "__FLIGHT_DATA"
24
+
25
+ # What counts as a mount target and what counts as a real call both live in
26
+ # {Ruact::LayoutSource}, shared with `ruact:install` so the runtime and the
27
+ # generator can never disagree about whether a layout is migrated.
28
+
29
+ private
30
+
31
+ # Emit the full HTML document a browser gets on a normal navigation.
32
+ #
33
+ # `Ruact.config.layout` decides, and it is EXPLICIT — ruact does not try
34
+ # to infer whether your layout is ready. It used to: three review rounds
35
+ # each found another template shape that fooled the inference (a mention
36
+ # in a comment, a commented-out call, a trim-mode comment), and each wrong
37
+ # answer governed how every page in the app rendered. Answering "does this
38
+ # template call this method?" is not something pattern-matching can do
39
+ # reliably, so the question is no longer asked. See
40
+ # Ruact::Configuration#layout.
41
+ #
42
+ # - `false` (the default) → the built-in shell. Byte-identical to ruact's
43
+ # behaviour before the layout path existed, with no detection in the
44
+ # way, so an app that has not opted in cannot be affected by any of this.
45
+ # - `true` / a String → render through the app's layout. `ruact:install`
46
+ # writes both halves of that opt-in together: `config.layout = true` in
47
+ # the initializer AND `<%= ruact_js_assets %>` in the layout.
48
+ #
49
+ # An opted-in layout that does not actually emit the assets would produce
50
+ # a blank page, so the rendered document is checked before it is
51
+ # committed: that is a configuration error, raised in development/test and
52
+ # logged-and-degraded in production rather than served to real traffic.
53
+ def render_ruact_document(payload)
54
+ layout = Ruact.config.layout
55
+ return render html: ruact_html_shell(payload).html_safe, layout: false if layout == false
56
+
57
+ unless ruact_layout_resolvable?(layout)
58
+ __ruact_handle_unready_layout(layout, :missing)
59
+ return render html: ruact_html_shell(payload).html_safe, layout: false
60
+ end
61
+
62
+ # Copied to the view by Rails' `view_assigns` plumbing (the name does not
63
+ # match the `/\A@_/` protected-ivar filter) — that is how the layout's
64
+ # zero-argument `ruact_js_assets` reaches THIS render's Flight payload.
65
+ @ruact_flight_payload = payload
66
+ document = render_to_string(html: "".html_safe, layout: layout)
67
+
68
+ if ruact_document_mountable?(document)
69
+ render html: document.html_safe, layout: false
70
+ else
71
+ __ruact_handle_unready_layout(layout, :unwired)
72
+ render html: ruact_html_shell(payload).html_safe, layout: false
73
+ end
74
+ ensure
75
+ remove_instance_variable(:@ruact_flight_payload) if instance_variable_defined?(:@ruact_flight_payload)
76
+ end
77
+
78
+ # A document is only usable if BOTH halves are present: the payload/bootstrap
79
+ # block AND something to mount into. Checking the assets alone accepts a
80
+ # layout that calls `ruact_js_assets` in `<head>` but never got the root div
81
+ # — React then boots with no mount target and the page is silently blank.
82
+ def ruact_document_mountable?(document)
83
+ document.include?(RUACT_ASSETS_MARKER) && Ruact::LayoutSource.root?(document)
84
+ end
85
+
86
+ # Is there a layout to render into at all? Asking FIRST matters: passing
87
+ # `layout: true` to a controller with no resolvable layout raises
88
+ # `ArgumentError("There was no default layout for ...")`, which would turn
89
+ # every page of an API-shaped or `layout false` controller into a 500 in
90
+ # an app that opted in globally. `_default_layout`'s `require_layout`
91
+ # argument defaults to false precisely so it can be used as a probe — it
92
+ # returns nil instead of raising, and honours `action_has_layout?`.
93
+ #
94
+ # `NameError` is deliberately NOT swallowed: `_default_layout` re-raises it
95
+ # as "Could not render layout: ..." to surface a broken layout resolver
96
+ # (`layout -> { MissingConstant::LAYOUT }`). Turning that into "no layout"
97
+ # would hide the developer's bug behind a silently degraded page.
98
+ def ruact_layout_resolvable?(layout)
99
+ return ruact_layout_exists?(layout) if layout.is_a?(String)
100
+
101
+ resolved = _default_layout(lookup_context, [:html], [])
102
+ return false if resolved.nil? || resolved == false
103
+ # `_default_layout` returns a Template for the conventional lookup but a
104
+ # bare String path for a controller-declared `layout "name"` — and it
105
+ # returns that String WITHOUT checking the template exists. Treating
106
+ # "not nil" as resolvable therefore sent a declared-but-missing layout
107
+ # into `render_to_string`, where it raised `MissingTemplate` instead of
108
+ # taking the intended degrade-to-shell path.
109
+ return ruact_layout_exists?(resolved) if resolved.is_a?(String)
110
+
111
+ true
112
+ rescue NameError
113
+ raise
114
+ rescue StandardError
115
+ false
116
+ end
117
+
118
+ def ruact_layout_exists?(name)
119
+ lookup_context.exists?(name.to_s.delete_prefix("layouts/"), ["layouts"])
120
+ end
121
+
122
+ def __ruact_handle_unready_layout(_layout, reason)
123
+ detail =
124
+ if reason == :missing
125
+ "no layout could be resolved for it"
126
+ else
127
+ "the layout it rendered emitted no `ruact_js_assets` output (or no " \
128
+ "`<div id=\"root\"></div>` to mount into), which would have been a blank page"
129
+ end
130
+
131
+ message = <<~MSG.strip
132
+ ruact: #{controller_path}##{action_name} fell back to ruact's built-in HTML shell — #{detail}.
133
+ `Ruact.config.layout` is set, so this is a configuration error, not a default:
134
+ the built-in shell has no stylesheet slot, so your app's CSS does not reach this page.
135
+ Add `<%= ruact_js_assets %>` next to the `<div id="root"></div>` in your layout
136
+ (`rails generate ruact:install` writes both; `rails ruact:doctor` reports what is missing),
137
+ or set `Ruact.configure { |c| c.layout = false }` to use the built-in shell deliberately.
138
+ MSG
139
+
140
+ # A MISSING layout is a legitimate per-controller choice — an API-shaped
141
+ # controller, or one that declared `layout false`, inside an app that
142
+ # opted in globally. Raising there would break a normal Rails pattern,
143
+ # so it degrades to the shell and says so where a developer will see it.
144
+ #
145
+ # An UNWIRED layout is different: the developer pointed ruact at a
146
+ # layout that cannot mount the app, which is their bug to see — loudly
147
+ # in development, and degraded (never blank) in production.
148
+ if reason == :missing
149
+ logger&.info(message) if __ruact_local_env?
150
+ return
151
+ end
152
+
153
+ raise Ruact::Error, message if __ruact_local_env?
154
+
155
+ logger&.error(message)
156
+ end
157
+
158
+ def __ruact_local_env?
159
+ Rails.env.development? || Rails.env.test?
160
+ rescue StandardError
161
+ false
162
+ end
163
+
164
+ def ruact_html_shell(flight_payload)
165
+ # Story 14.2 — the JS asset block (entry `<script>` tags + `__FLIGHT_DATA`)
166
+ # is delegated to the single `Ruact::ViewHelper#ruact_js_assets`
167
+ # implementation. The bootstrap entry script is a deferred ES module, so it
168
+ # runs after the inline `__FLIGHT_DATA` classic script has populated the
169
+ # queue regardless of source order — emitting the whole block in `<body>`
170
+ # is correct.
171
+ <<~HTML
172
+ <!DOCTYPE html>
173
+ <html lang="en">
174
+ <head>
175
+ <meta charset="UTF-8" />
176
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
177
+ #{ruact_csrf_meta_tag}
178
+ <title>Rails RSC</title>
179
+ </head>
180
+ <body>
181
+ <div id="root"></div>
182
+ #{ruact_js_assets(flight_payload)}
183
+ </body>
184
+ </html>
185
+ HTML
186
+ end
187
+
188
+ # Story 8.3 review R7 — emits `<meta name="csrf-token" content="...">`
189
+ # into the shell so the JS runtime's `<meta>` lookup can forward a
190
+ # valid `X-CSRF-Token` on every server-function (mutation) call. Without
191
+ # this, hosts that route `ruact_render` through the gem's HTML shell (the
192
+ # standard path) have no token in the document and the host's
193
+ # `protect_from_forgery` rejects every non-GET server function.
194
+ #
195
+ # Returns an empty string when CSRF protection isn't available
196
+ # (non-Rails specs, or hosts that have deliberately stripped
197
+ # `form_authenticity_token` from the controller surface).
198
+ def ruact_csrf_meta_tag
199
+ return "" unless respond_to?(:form_authenticity_token, true)
200
+
201
+ token = form_authenticity_token
202
+ return "" if token.nil? || token.empty?
203
+
204
+ %(<meta name="csrf-token" content="#{ERB::Util.html_escape(token)}" />)
205
+ rescue StandardError
206
+ ""
207
+ end
208
+ end
209
+ end
210
+ end
@@ -5,6 +5,7 @@ require "socket"
5
5
  require "uri"
6
6
  require_relative "view_helper"
7
7
  require_relative "validation_errors_collector"
8
+ require_relative "controller/document_rendering"
8
9
 
9
10
  module Ruact
10
11
  # Include in ApplicationController to enable RSC rendering.
@@ -27,6 +28,9 @@ module Ruact
27
28
  # and so the shared `vite_dev_running?` / `vite_manifest_entry` helpers are
28
29
  # reachable here without duplication.
29
30
  include Ruact::ViewHelper
31
+ # Who owns the `<head>` — the host app's layout, with ruact's built-in shell
32
+ # as the un-migrated fallback. See Ruact::Controller::DocumentRendering.
33
+ include Ruact::Controller::DocumentRendering
30
34
 
31
35
  private
32
36
 
@@ -178,7 +182,7 @@ module Ruact
178
182
  if ruact_request?
179
183
  render plain: payload, content_type: "text/x-component"
180
184
  else
181
- render html: ruact_html_shell(payload).html_safe, layout: false
185
+ render_ruact_document(payload)
182
186
  end
183
187
  end
184
188
  end
@@ -236,50 +240,5 @@ module Ruact
236
240
  controller = self.class.name.underscore.sub("_controller", "")
237
241
  Rails.root.join("app", "views", controller, "#{action}.html.erb")
238
242
  end
239
-
240
- def ruact_html_shell(flight_payload)
241
- # Story 14.2 — the JS asset block (entry `<script>` tags + `__FLIGHT_DATA`)
242
- # is delegated to the single `Ruact::ViewHelper#ruact_js_assets`
243
- # implementation. The bootstrap entry script is a deferred ES module, so it
244
- # runs after the inline `__FLIGHT_DATA` classic script has populated the
245
- # queue regardless of source order — emitting the whole block in `<body>`
246
- # is correct.
247
- <<~HTML
248
- <!DOCTYPE html>
249
- <html lang="en">
250
- <head>
251
- <meta charset="UTF-8" />
252
- <meta name="viewport" content="width=device-width, initial-scale=1" />
253
- #{ruact_csrf_meta_tag}
254
- <title>Rails RSC</title>
255
- </head>
256
- <body>
257
- <div id="root"></div>
258
- #{ruact_js_assets(flight_payload)}
259
- </body>
260
- </html>
261
- HTML
262
- end
263
-
264
- # Story 8.3 review R7 — emits `<meta name="csrf-token" content="...">`
265
- # into the shell so the JS runtime's `<meta>` lookup can forward a
266
- # valid `X-CSRF-Token` on every server-function (mutation) call. Without
267
- # this, hosts that route `ruact_render` through the gem's HTML shell (the
268
- # standard path) have no token in the document and the host's
269
- # `protect_from_forgery` rejects every non-GET server function.
270
- #
271
- # Returns an empty string when CSRF protection isn't available
272
- # (non-Rails specs, or hosts that have deliberately stripped
273
- # `form_authenticity_token` from the controller surface).
274
- def ruact_csrf_meta_tag
275
- return "" unless respond_to?(:form_authenticity_token, true)
276
-
277
- token = form_authenticity_token
278
- return "" if token.nil? || token.empty?
279
-
280
- %(<meta name="csrf-token" content="#{ERB::Util.html_escape(token)}" />)
281
- rescue StandardError
282
- ""
283
- end
284
243
  end
285
244
  end
data/lib/ruact/doctor.rb CHANGED
@@ -171,14 +171,46 @@ module Ruact
171
171
  end
172
172
  end
173
173
 
174
+ # Two independent halves have to line up, and BOTH are silent when wrong:
175
+ # the layout has to call `ruact_js_assets`, and `Ruact.config.layout` has to
176
+ # be on. Miss either and ruact renders its built-in shell — which carries no
177
+ # stylesheet, so the app's own CSS never reaches a ruact page and nothing
178
+ # errors. Reporting each half separately is the point: "add one line" and
179
+ # "flip one setting" are different fixes.
180
+ #
181
+ # `Ruact::LayoutSource` is the SHARED definition of "calls the helper" —
182
+ # the runtime and `ruact:install` read it too, so this check cannot drift
183
+ # into disagreeing with what actually happens at render time (it did:
184
+ # a `<%# TODO: add ruact_js_assets %>` comment used to pass here).
174
185
  def check_layout
175
186
  path = Rails.root.join("app", "views", "layouts", "application.html.erb")
176
- if File.exist?(path) && File.read(path).include?("ruact: root")
177
- [:pass, "React shell present in application.html.erb"]
178
- else
179
- [:fail, "React shell missing from application.html.erb",
180
- "Run rails generate ruact:install to add the React shell to application.html.erb."]
187
+ unless File.exist?(path)
188
+ return [:fail, "React shell missing from application.html.erb",
189
+ "Run rails generate ruact:install to add the React shell to application.html.erb."]
181
190
  end
191
+
192
+ content = File.read(path)
193
+ has_root = Ruact::LayoutSource.root?(content)
194
+ has_assets = Ruact::LayoutSource.wired?(content)
195
+ opted_in = Ruact.config.layout != false
196
+
197
+ return layout_unwired_result unless has_root && has_assets
198
+ return layout_not_opted_in_result unless opted_in
199
+
200
+ [:pass, "layout owns the document (React root + ruact_js_assets, config.layout on)"]
201
+ end
202
+
203
+ def layout_unwired_result
204
+ [:fail, "layout is missing the React root and/or the ruact_js_assets call",
205
+ "Add <%= ruact_js_assets %> next to <div id=\"root\"></div> in " \
206
+ "app/views/layouts/application.html.erb (or re-run rails generate ruact:install). " \
207
+ "Without both, ruact renders its built-in shell and your app's CSS never reaches the page."]
208
+ end
209
+
210
+ def layout_not_opted_in_result
211
+ [:warn, "layout is ready but Ruact.config.layout is false",
212
+ "Your layout calls ruact_js_assets, but ruact is still rendering its built-in shell " \
213
+ "(which has no stylesheet). Set `config.layout = true` in config/initializers/ruact.rb."]
182
214
  end
183
215
 
184
216
  def check_streaming
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruact
4
+ # Answers one question in ONE place: does this layout actually wire ruact up?
5
+ #
6
+ # Two callers need that answer and must not disagree about it — the runtime
7
+ # (`Ruact::Controller::DocumentRendering`, deciding whether it is safe to
8
+ # render through the host layout) and `ruact:install` (deciding whether the
9
+ # layout still needs migrating). When they each carried their own notion of
10
+ # "present", they drifted: the generator skipped a layout as already-migrated
11
+ # on a `<%# TODO: add ruact_js_assets %>` comment, while the runtime read the
12
+ # same layout as unwired. Both were string-matching a NAME where only a CALL
13
+ # counts.
14
+ module LayoutSource
15
+ # ERB comments are stripped before anything else. A commented-out call —
16
+ # `<%# <%= ruact_js_assets %> %>`, which a developer produces the moment
17
+ # they disable it while debugging — emits NOTHING, so counting it as wired
18
+ # would report a layout as migrated when it is not. The `-?` covers ERB's
19
+ # trim-mode forms (`<%-#` / `-%>`), which Erubi treats as comments too and
20
+ # an earlier version of this pattern missed.
21
+ ERB_COMMENT = /<%-?#.*?-?%>/m
22
+
23
+ # An ERB OUTPUT tag calling the helper: `<%= ruact_js_assets %>` and the raw
24
+ # `<%== ... %>` form, with or without arguments or surrounding whitespace.
25
+ # Scanning stops at the tag's own `%>` rather than forbidding `%` outright —
26
+ # `<%= raw("100%") + ruact_js_assets %>` is a legitimate call that a
27
+ # `[^%]*` pattern rejected, while still never matching a bare mention that
28
+ # merely follows some other tag.
29
+ ASSETS_CALL = /<%=+(?:(?!%>).)*?\bruact_js_assets\b/m
30
+
31
+ # The React mount target, as an attribute rather than as a substring. The
32
+ # lookbehind is what stops `data-id="root"` (and any other `*-id`) from
33
+ # counting: those are not the mount point, and a document that has one but
34
+ # no real root gives React nothing to mount into. Unquoted `id=root` is
35
+ # valid HTML and is accepted.
36
+ ROOT_ATTRIBUTE = /(?<![-\w])id\s*=\s*(?:"root"|'root'|root(?=[\s>]))/
37
+
38
+ # The whole element, for a generator that needs something to inject AFTER.
39
+ ROOT_ELEMENT = %r{<div\s[^>]*#{ROOT_ATTRIBUTE.source}[^>]*>\s*</div>}
40
+
41
+ class << self
42
+ # Does this ERB source actually CALL `ruact_js_assets`?
43
+ def wired?(source)
44
+ without_comments(source).match?(ASSETS_CALL)
45
+ end
46
+
47
+ # Does this markup carry a React mount target? Used on RENDERED HTML by
48
+ # the runtime and on ERB source by the generator; the attribute shape is
49
+ # the same either way.
50
+ def root?(markup)
51
+ markup.to_s.match?(ROOT_ATTRIBUTE)
52
+ end
53
+
54
+ def without_comments(source)
55
+ source.to_s.gsub(ERB_COMMENT, "")
56
+ end
57
+ end
58
+ end
59
+ end
data/lib/ruact/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruact
4
- VERSION = "0.0.8"
4
+ VERSION = "0.0.9"
5
5
  end
@@ -44,12 +44,21 @@ module Ruact
44
44
  # (no drift). Available in every view via the railtie's
45
45
  # `ActionView::Base.include(Ruact::ViewHelper)`.
46
46
  #
47
+ # Called from a LAYOUT the argument is normally omitted: the payload for the
48
+ # render in flight is picked up from `@ruact_flight_payload`, which
49
+ # `Ruact::Controller#render_ruact_document` sets on the controller and Rails
50
+ # copies to the view (the name does not match the `/\A@_/` protected-ivar
51
+ # filter). On a plain Rails page — no ruact render in flight — that ivar is
52
+ # absent and the helper emits only the entry tags, exactly as before.
53
+ #
47
54
  # @param flight_payload [String, nil] the per-render Flight wire payload to
48
- # inline as `__FLIGHT_DATA`; omit (or pass nil) to emit only the entry tags.
55
+ # inline as `__FLIGHT_DATA`. Omit inside a layout to use the render in
56
+ # flight; an explicit argument always wins.
49
57
  # @return [ActiveSupport::SafeBuffer] the asset markup, html_safe
50
58
  # @example In a layout
51
59
  # <%= ruact_js_assets %>
52
60
  def ruact_js_assets(flight_payload = nil)
61
+ flight_payload ||= @ruact_flight_payload
53
62
  parts = []
54
63
  parts << ruact_flight_data_script(flight_payload) unless flight_payload.nil?
55
64
  parts << ruact_vite_tags
data/lib/ruact.rb CHANGED
@@ -7,6 +7,7 @@ require_relative "ruact/serializable"
7
7
  require_relative "ruact/signed_references"
8
8
  require_relative "ruact/flight"
9
9
  require_relative "ruact/string_distance"
10
+ require_relative "ruact/layout_source"
10
11
  require_relative "ruact/component_contract"
11
12
  require_relative "ruact/erb_preprocessor"
12
13
  require_relative "ruact/render_context"
@@ -0,0 +1,3 @@
1
+ <div>
2
+ <DemoButton label={"hello"} />
3
+ </div>
@@ -0,0 +1,3 @@
1
+ <div>
2
+ <DemoButton label={"hello"} />
3
+ </div>
@@ -0,0 +1,3 @@
1
+ <div>
2
+ <DemoButton label={"hello"} />
3
+ </div>
@@ -0,0 +1,3 @@
1
+ <div>
2
+ <DemoButton label={"hello"} />
3
+ </div>
@@ -0,0 +1,3 @@
1
+ <div>
2
+ <DemoButton label={"hello"} />
3
+ </div>
@@ -0,0 +1,16 @@
1
+ <%# An UNMIGRATED host layout: it has the React root but never calls
2
+ `ruact_js_assets`, so rendering the page through it would produce a
3
+ document with no bootstrap entry and no Flight payload — a blank page.
4
+ ruact must detect that and fall back to its built-in shell instead. %>
5
+ <!DOCTYPE html>
6
+ <html>
7
+ <head>
8
+ <link rel="stylesheet" href="/host-app.css" />
9
+ <title>Host App Title</title>
10
+ </head>
11
+ <body>
12
+ <%= yield %>
13
+ <%# ruact: root %>
14
+ <div id="root"></div>
15
+ </body>
16
+ </html>
@@ -0,0 +1,24 @@
1
+ <%# An UNMIGRATED layout that cannot survive being rendered on a ruact page.
2
+ It reads an ivar that only a plain Rails action would have set — exactly the
3
+ shape of a real pre-migration app, whose layout has never once run on a
4
+ ruact-rendered page. If ruact renders this speculatively just to discover it
5
+ lacks `ruact_js_assets`, the app 500s on a page that used to work.
6
+
7
+ NOTE: this comment MENTIONS the helper name on purpose — a readiness check
8
+ that greps for the bare string counts this layout as wired and renders it. %>
9
+ <%# <%= ruact_js_assets %> %>
10
+ <%# ...and the line above is a genuinely COMMENTED-OUT call, which is the
11
+ sharper version of the same trap: ERB comments do not nest, so it emits
12
+ nothing, but a check that only looks for `<%=` sees a call. Both shapes must
13
+ read as UNMIGRATED, or this layout gets rendered and 500s below. %>
14
+ <!DOCTYPE html>
15
+ <html>
16
+ <head>
17
+ <title><%= @page_title.upcase %></title>
18
+ </head>
19
+ <body>
20
+ <%= yield %>
21
+ <%# ruact: root %>
22
+ <div id="root"></div>
23
+ </body>
24
+ </html>
@@ -0,0 +1,15 @@
1
+ <%# A layout that calls `ruact_js_assets` but never got the React root div.
2
+ The payload and the bootstrap both ship, so an assets-only readiness check
3
+ accepts it — and the browser boots React with nothing to mount into,
4
+ producing a silently blank page. %>
5
+ <!DOCTYPE html>
6
+ <html>
7
+ <head>
8
+ <link rel="stylesheet" href="/host-app.css" />
9
+ <title>Host App Title</title>
10
+ </head>
11
+ <body>
12
+ <%= yield %>
13
+ <%= ruact_js_assets %>
14
+ </body>
15
+ </html>
@@ -0,0 +1,17 @@
1
+ <%# A MIGRATED host layout: the app owns the document. The stylesheet link is
2
+ the whole point — it is the thing ruact's built-in shell has no slot for,
3
+ so a spec asserting it appears in the response is asserting that the host
4
+ app's CSS actually reaches a ruact page. %>
5
+ <!DOCTYPE html>
6
+ <html>
7
+ <head>
8
+ <link rel="stylesheet" href="/host-app.css" />
9
+ <title>Host App Title</title>
10
+ </head>
11
+ <body>
12
+ <%= yield %>
13
+ <%# ruact: root %>
14
+ <div id="root"></div>
15
+ <%= ruact_js_assets %>
16
+ </body>
17
+ </html>